<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
<meta name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">

    <meta name="author" content="Ian Tang">


    <meta name="subtitle" content="岁月不居时节如流">


    <meta name="description" content="好记性不如烂笔头">



<title>计算机系统 | 随笔</title>



    <link rel="icon" href="/image/The_Lion_King_32_32.png">




    <!-- stylesheets list from _config.yml -->
    
    <link rel="stylesheet" href="/css/style.css">
    



    <!-- scripts list from _config.yml -->
    
    <script src="/js/script.js"></script>
    
    <script src="/js/tocbot.min.js"></script>
    



    
    
        
    


<meta name="generator" content="Hexo 6.3.0"></head>

<body>
    <script>
        // this function is used to check current theme before page loaded.
        (() => {
            const currentTheme = window.localStorage && window.localStorage.getItem('theme') || '';
            const isDark = currentTheme === 'dark';
            const pagebody = document.getElementsByTagName('body')[0]
            if (isDark) {
                pagebody.classList.add('dark-theme');
                // mobile
                document.getElementById("mobile-toggle-theme").innerText = "· Dark"
            } else {
                pagebody.classList.remove('dark-theme');
                // mobile
                document.getElementById("mobile-toggle-theme").innerText = "· Light"
            }
        })();
    </script>

    <div class="wrapper">
        <header>
    <nav class="navbar">
        <div class="container">
            <div class="navbar-header header-logo"><a href="/">Tangkaifei&#39;s Blog</a></div>
            <div class="menu navbar-right">
                
                    <a class="menu-item" href="/archives">Posts</a>
                
                    <a class="menu-item" href="/category">Categories</a>
                
                    <a class="menu-item" href="/tag">Tags</a>
                
                    <a class="menu-item" href="/about">About</a>
                
                <input id="switch_default" type="checkbox" class="switch_default">
                <label for="switch_default" class="toggleBtn"></label>
            </div>
        </div>
    </nav>

    
    <nav class="navbar-mobile" id="nav-mobile">
        <div class="container">
            <div class="navbar-header">
                <div>
                    <a href="/">Tangkaifei&#39;s Blog</a><a id="mobile-toggle-theme">·&nbsp;Light</a>
                </div>
                <div class="menu-toggle" onclick="mobileBtn()">&#9776; Menu</div>
            </div>
            <div class="menu" id="mobile-menu">
                
                    <a class="menu-item" href="/archives">Posts</a>
                
                    <a class="menu-item" href="/category">Categories</a>
                
                    <a class="menu-item" href="/tag">Tags</a>
                
                    <a class="menu-item" href="/about">About</a>
                
            </div>
        </div>
    </nav>

</header>
<script>
    var mobileBtn = function f() {
        var toggleMenu = document.getElementsByClassName("menu-toggle")[0];
        var mobileMenu = document.getElementById("mobile-menu");
        if(toggleMenu.classList.contains("active")){
           toggleMenu.classList.remove("active")
            mobileMenu.classList.remove("active")
        }else{
            toggleMenu.classList.add("active")
            mobileMenu.classList.add("active")
        }
    }
</script>
            <div class="main">
                <div class="container">
    
    
        <div class="post-toc">
    <div class="tocbot-list">
    </div>
    <div class="tocbot-list-menu">
        <a class="tocbot-toc-expand" onclick="expand_toc()">Expand all</a>
        <a onclick="go_top()">Back to top</a>
        <a onclick="go_bottom()">Go to bottom</a>
    </div>
</div>

<script>
    var tocbot_timer;
    var DEPTH_MAX = 6; // 为 6 时展开所有
    var tocbot_default_config = {
        tocSelector: '.tocbot-list',
        contentSelector: '.post-content',
        headingSelector: 'h1, h2, h3, h4, h5',
        orderedList: false,
        scrollSmooth: true,
        onClick: extend_click,
    };

    function extend_click() {
        clearTimeout(tocbot_timer);
        tocbot_timer = setTimeout(function() {
            tocbot.refresh(obj_merge(tocbot_default_config, {
                hasInnerContainers: true
            }));
        }, 420); // 这个值是由 tocbot 源码里定义的 scrollSmoothDuration 得来的
    }

    document.ready(function() {
        tocbot.init(obj_merge(tocbot_default_config, {
            collapseDepth: 1
        }));
    });

    function expand_toc() {
        var b = document.querySelector('.tocbot-toc-expand');
        var expanded = b.getAttribute('data-expanded');
        expanded ? b.removeAttribute('data-expanded') : b.setAttribute('data-expanded', true);
        tocbot.refresh(obj_merge(tocbot_default_config, {
            collapseDepth: expanded ? 1 : DEPTH_MAX
        }));
        b.innerText = expanded ? 'Expand all' : 'Collapse all';
    }

    function go_top() {
        window.scrollTo(0, 0);
    }

    function go_bottom() {
        window.scrollTo(0, document.body.scrollHeight);
    }

    function obj_merge(target, source) {
        for (var item in source) {
            if (source.hasOwnProperty(item)) {
                target[item] = source[item];
            }
        }
        return target;
    }
</script>
    

    
    <article class="post-wrap">
        <header class="post-header">
            <h1 class="post-title">计算机系统</h1>
            
                <div class="post-meta">
                    
                        Author: <a itemprop="author" rel="author" href="/">Ian Tang</a>
                    

                    
                        <span class="post-time">
                        Date: <a href="#">十一月 27, 2022&nbsp;&nbsp;11:46:00</a>
                        </span>
                    
                    
                        <span class="post-category">
                    Category:
                            
                                <a href="/categories/%E6%8A%80%E6%9C%AF%EF%BC%9A%E7%9F%A5%E8%AF%86%E6%8A%80%E8%83%BD/">技术：知识技能</a>
                            
                        </span>
                    
                </div>
            
        </header>

        <div class="post-content">
            <h1 id="计算机硬件系统"><a href="#计算机硬件系统" class="headerlink" title="计算机硬件系统"></a>计算机硬件系统</h1><p>操作系统与运行操作系统的内核硬件关系密切。操作系统扩展了计算机指令集并管理计算机的资源。</p>
<h2 id="CPU"><a href="#CPU" class="headerlink" title="CPU"></a>CPU</h2><p>每个 CPU 都有一组可以执行的特定指令集。因此，x86 的 CPU 不能执行 ARM 的程序并且 ARM 的 CPU 也不能执行 x86 的程序。由于访问内存获取执行数据要比执行指令花费的时间长，因此所有的 CPU 内部都会包含一些<code>寄存器</code>来保存关键变量和临时结果。因此，在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中，以及把关键字从寄存器存入到内存中。还有一些其他的指令会把来自寄存器和内存的操作数进行组合，例如 add 操作就会把两个操作数相加并把结果保存到内存中。</p>
<ul>
<li><p>通用寄存器</p>
</li>
<li><p>状态寄存器 当前线程运行的一系列关键信息</p>
</li>
<li><p>指令寄存器 </p>
</li>
<li><p>段寄存器</p>
<p>8086CPU分为两个内存模式，一个是实模式，一个是保护模式。</p>
<p>实模式是将段寄存器作为20位地址的高四位地址（段基地址），通用寄存器作为低16为地址进行寻址操作，一般是在CPU复位或者加电时处于这种模式，只能执行单进程操作，便于调试。</p>
<p>保护模式是将段寄存器作为段选择子，指示段寄存器指向哪个分段</p>
</li>
</ul>
<p>另一个寄存器是 <code>堆栈指针(stack pointer)</code>，它指向内存中当前栈的顶端。堆栈指针会包含输入过程中的有关参数、局部变量以及没有保存在寄存器中的临时变量。</p>
<p>操作系统必须了解所有的寄存器。在<code>时间多路复用(time multiplexing)</code> 的 CPU 中，操作系统往往停止运行一个程序转而运行另外一个。每次当操作系统停止运行一个程序时，操作系统会保存所有寄存器的值，以便于后续重新运行该程序。</p>
<p>为了提升性能， CPU 设计人员早就放弃了同时去读取、解码和执行一条简单的指令。许多现代的 CPU 都具有同时读取多条指令的机制。例如<strong>，一个 CPU 可能会有单独访问、解码和执行单元，所以，当 CPU 执行第 N 条指令时，还可以对 N + 1 条指令解码，还可以读取 N + 2 条指令。像这样的组织形式被称为 <code>流水线(pipeline)。</code></strong></p>
<p>除了用在嵌入式系统中非常简单的 CPU 之外，多数 CPU 都有<code>两种模式</code>，即前面已经提到的内核态和用户态。通常情况下，<code>PSW 寄存器</code>中的一个二进制位会控制当前状态是内核态还是用户态。当运行在内核态时，CPU 能够执行任何指令集中的指令并且能够使用硬件的功能。在台式机和服务器上，操作系统通常以内核模式运行，从而可以访问完整的硬件。在大多数嵌入式系统中，一部分运行在内核态下，剩下的一部分运行在用户态下。</p>
<p>用户应用程序通常运行在用户态下，在用户态下，CPU 只能执行指令集中的一部分并且只能访问硬件的一部分功能。一般情况下，在用户态下，有关 I&#x2F;O 和内存保护的所有指令是禁止执行的。当然，设置 PSW 模式的二进制位为内核态也是禁止的。</p>
<p>为了获取操作系统的服务，用户程序必须使用 <code>系统调用(system call)</code>，系统调用会转换为内核态并且调用操作系统。<code>TRAP</code> 指令用于把用户态切换为内核态并启用操作系统。当有关工作完成之后，在系统调用后面的指令会把控制权交给用户程序。</p>
<p>需要注意的是操作系统在进行系统调用时会存在陷阱。大部分的陷阱会导致硬件发出警告，比如说试图被零除或浮点下溢等你。在所有的情况下，操作系统都能得到控制权并决定如何处理异常情况。有时，由于出错的原因，程序不得不停止。</p>
<p>多线程允许 CPU 保持两个不同的线程状态并且在<code>纳秒级(nanosecond)</code> 的时间完成切换。线程是一种轻量级的进程，我们会在后面说到。例如，如果一个进程想要从内存中读取指令(这通常会经历几个时钟周期)，多线程 CPU 则可以切换至另一个线程。多线程不会提供真正的并行处理。在一个时刻只有一个进程在运行。</p>
<p>对于操作系统来讲，多线程是有意义的，因为每个线程对操作系统来说都像是一个单个的 CPU。比如一个有两个 CPU 的操作系统，并且每个 CPU 运行两个线程，那么这对于操作系统来说就可能是 4 个 CPU。</p>
<h2 id="内存（存储器）"><a href="#内存（存储器）" class="headerlink" title="内存（存储器）"></a>内存（存储器）</h2><p>一般来说高速缓存器使用SRAM；内存使用DRAM；硬盘使用磁盘或者SSD。</p>
<p>计算机中第二个主要的组件就是内存。理想情况下，内存应该非常快速(比执行一条指令要快，从而不会拖慢 CPU 执行效率)，而且足够大且便宜，但是目前的技术手段无法满足三者的需求。于是采用了不同的处理方式，存储器系统采用一种分层次的结构</p>
<p>顶层的存储器速度最高，但是容量最小，成本非常高，层级结构越向下，其访问效率越慢，容量越大，但是造价也就越便宜：</p>
<h3 id="寄存器"><a href="#寄存器" class="headerlink" title="寄存器"></a>寄存器</h3><p>存储器的顶层是 CPU 中的<code>寄存器</code>，它们用和 CPU 一样的材料制成，所以和 CPU 一样快。程序必须在软件中自行管理这些寄存器（即决定如何使用它们）</p>
<h3 id="高速缓存cache"><a href="#高速缓存cache" class="headerlink" title="高速缓存cache"></a>高速缓存cache</h3><p>位于寄存器下面的是<code>高速缓存</code>，它多数由硬件控制。主存被分割成<code>高速缓存行(cache lines)</code> 为 64 字节，内存地址的 0 - 63 对应高速缓存行 0 ，地址 64 - 127 对应高速缓存行的 1，等等。使用最频繁的高速缓存行保存在位于 CPU 内部或非常靠近 CPU 的高速缓存中。当应用程序需要从内存中读取关键词的时候，高速缓存的硬件会检查所需要的高速缓存行是否在高速缓存中。如果在的话，那么这就是<code>高速缓存命中(cache hit)</code>。高速缓存满足了该请求，并且没有通过总线将内存请求发送到主内存。高速缓存命中通常需要花费两个时钟周期。缓存未命中需要从内存中提取，这会消耗大量的时间。高速缓存行会限制容量的大小因为它的造价非常昂贵。有一些机器会有两个或者三个高速缓存级别，每一级高速缓存比前一级慢且容量更大。</p>
<p>缓存在计算机很多领域都扮演了非常重要的角色，不仅仅是 RAM 缓存行。</p>
<p>缓存是解决问题的一种好的方式，所以现代 CPU 设计了两种缓存。第一级缓存或者说是 <code>L1 cache</code> 总是位于 CPU 内部，<strong>用来将已解码的指令调入 CPU 的执行引擎</strong>。对于那些频繁使用的关键字，多数芯片有第二个 L1 cache 。典型的 L1 cache 的大小为 16 KB。另外，往往还设有二级缓存，也就是 <code>L2 cache</code>，用来存放最近使用过的关键字，一般是兆字节为单位。L1 cache 和 L2 cache 最大的不同在于是否存在延迟。访问 L1 cache 没有任何的延迟，然而访问 L2 cache 会有 1 - 2 个时钟周期的延后。</p>
<blockquote>
<p>什么是时钟周期？计算机处理器或 CPU 的速度由时钟周期来确定，该时钟周期是振荡器两个脉冲之间的时间量。一般而言，每秒脉冲数越高，计算机处理器处理信息的速度就越快。时钟速度以 Hz 为单位测量，通常为兆赫（MHz）或千兆赫（GHz）。例如，一个4 GHz处理器每秒执行4,000,000,000个时钟周期。</p>
<p>计算机处理器可以在每个时钟周期执行一条或多条指令，这具体取决于处理器的类型。早期的计算机处理器和较慢的 CPU 在每个时钟周期只能执行一条指令，而现代处理器在每个时钟周期可以执行多条指令。</p>
</blockquote>
<h3 id="主存-内存RAM"><a href="#主存-内存RAM" class="headerlink" title="主存(内存RAM)"></a>主存(内存RAM)</h3><p>在上面的层次结构中再下一层是<code>主存</code>，这是内存系统的主力军，主存通常叫做 <code>RAM(Random Access Memory)</code></p>
<p>所有不能再高速缓存中得到满足的内存访问请求都会转往主存中</p>
<p>除了主存之外，许多计算机还具有少量的非易失性随机存取存储器。它们与 RAM 不同，在电源断电后，非易失性随机访问存储器并不会丢失内容，就是<strong>闪存</strong>，例如u盘，固态硬盘。<code>ROM(Read Only Memory)</code> 中的内容一旦存储后就不会再被修改。它非常快而且便宜。（如果有人问你，有没有什么又快又便宜的内存设备，那就是 ROM 了）在计算机中，用于启动计算机的引导加载模块（也就是 bootstrap ）就存放在 ROM 中。另外，一些 I&#x2F;O 卡也采用 ROM 处理底层设备控制。</p>
<h3 id="磁盘"><a href="#磁盘" class="headerlink" title="磁盘"></a>磁盘</h3><p>下一个层次是<code>磁盘(硬盘)</code>，磁盘同 RAM 相比，每个二进制位的成本低了两个数量级，而且经常也有两个数量级大的容量。磁盘唯一的问题是随机访问数据时间大约慢了三个数量级。磁盘访问慢的原因是因为磁盘的构造不同</p>
<p>磁盘是一种机械装置，在一个磁盘中有一个或多个金属盘片，它们以 5400rpm、7200rpm、10800rpm 或更高的速度旋转。从边缘开始有一个机械臂悬横在盘面上，这类似于老式播放塑料唱片 33 转唱机上的拾音臂。信息会写在磁盘一系列的同心圆上。在任意一个给定臂的位置，每个磁头可以读取一段环形区域，称为<code>磁道(track)</code>。把一个给定臂的位置上的所有磁道合并起来，组成了一个<code>柱面(cylinder)</code>。</p>
<p>每个磁道划分若干扇区，扇区的值是 512 字节。在现代磁盘中，较外部的柱面比较内部的柱面有更多的扇区。机械臂从一个柱面移动到相邻的柱面大约需要 1ms。而随机移到一个柱面的典型时间为 5ms 至 10ms，具体情况以驱动器为准。一旦磁臂到达正确的磁道上，驱动器必须等待所需的扇区旋转到磁头之下，就开始读写，低端硬盘的速率是<code>50MB/s</code>，而高速磁盘的速率是 <code>160MB/s</code>。</p>
<blockquote>
<p>需要注意，<code>固态硬盘(Solid State Disk, SSD)</code>不是磁盘，固态硬盘并没有可以移动的部分，外形也不像唱片，并且数据是存储在<code>存储器（闪存）</code>中，与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭也不会丢失的数据。</p>
</blockquote>
<p>许多计算机支持一种著名的<code>虚拟内存</code>机制，这种机制使得期望运行的存储空间大于实际的物理存储空间。其方法是将程序放在磁盘上，而将主存作为一部分缓存，用来保存最频繁使用的部分程序，这种机制需要快速映像内存地址，用来把程序生成的地址转换为有关字节在 RAM 中的物理地址。这种映像由 CPU 中的一个称为 <code>存储器管理单元(Memory Management Unit, MMU)</code> 的部件来完成。</p>
<p>缓存和 MMU 的出现是对系统的性能有很重要的影响，在多道程序系统中，从一个程序切换到另一个程序的机制称为 <code>上下文切换(context switch)</code>，对来自缓存中的资源进行修改并把其写回磁盘是很有必要的。</p>
<p>存储器层次结构从上到下：</p>
<p>CPU寄存器（与CPU同样的材质，cpu可以直接读取） –&gt;L0高速缓冲（SRAM，通常需要几个时钟周期）–&gt;L1高速缓存 –&gt;L2高速缓存 –&gt;主存（内存，采用DRAM） –&gt;本地磁盘 –&gt;远程磁盘</p>
<p>每层存储结构都是作为下一层的缓存。</p>
<p><strong>什么是缓存？</strong></p>
<p>在K+1层的存储器被划分为连续的数据对象组块(chunk)，称为块（block）。每个块都有一个唯一的地址或者名字，块是固定大小的，也可以是可变大小。类似的在K层的存储器被划分成较少的块的集合，块大小与K+1层一样，K层的缓存就是K+1层部分块的副本。</p>
<p>在相邻的两个存储层次之间的块是一致的，但是与其他层次之间可以有不同的块大小。例如L0与L1层之间的块是几个字节大小，而L1和L2之间的块就是几十个字节大小。</p>
<ol>
<li><p>缓存命中cache hit</p>
<p>如果cpu需要取K+1层里面的数据时，它会先在K层寻找副本，如果找到就是缓存命中。</p>
</li>
<li><p>缓存不命中cache kiss</p>
<p>如果K层没有缓存需要的数据，这就是缓存不命中。K层会从K+1层中取出需要的缓存，如果K层存储满了，就会覆盖一个块，这叫做替换或者驱逐，被替换掉的叫牺牲块，替换哪一个有替换策略控制</p>
</li>
<li><p>缓存不命中的种类包括：冷不命中和冲突不命中</p>
<p>冷不命中：就是K层缓存块里没有缓存任何数据</p>
<p>冲突不命中：在K层缓存的时候并不是随意缓存下一层的数据，而是有一个放置策略，例如K层 <strong>i</strong> 缓存块只会缓存下一层被4整除的数据块，则<strong>i</strong>缓存的就是0、4、8、12块等，如果在程序执行过程中，有一个循环，不停的访问一个数组，这个数组比较大，分放在K+1存储器4、8两个块内，则在K层只会缓存4或者8其中的一个，就会造成不停的缓存不命中，这就是冲突不命中。这很明显就是缓存块设置的太小，像DPDK里面的改进就是有一项大页设置，设置比较大的缓存块。</p>
</li>
<li><p>缓存管理</p>
<p>设立存储器结构层次就是为了缓存下一层存储器，因此必须有某种逻辑管理缓存，具体就是划分成块，传送块，判断是否命中，并处理。处理逻辑可以是硬件也可以是软件。</p>
<p>编译器管理寄存器文件。</p>
<p>L1、L2、L3是由内置在缓存中的硬件逻辑来管理。</p>
<p>在使用虚拟内存（基本现在操作系统都使用）的系统里，DRAM内存是作为磁盘上数据块的缓存，由操作系统软件和CPU内地址翻译寄存器TLB的硬件共同管理。</p>
<p>本次磁盘缓存远程磁盘的缓存管理由像AFS这样的分布式文件系统</p>
</li>
</ol>
<h2 id="I-x2F-O-设备"><a href="#I-x2F-O-设备" class="headerlink" title="I&#x2F;O 设备"></a>I&#x2F;O 设备</h2><p>I&#x2F;O 设备一般包括两个部分：设备控制器和设备本身。控制器本身是一块芯片或者一组芯片，它能够控制物理设备。它能够接收操作系统的指令，例如，从设备中读取数据并完成数据的处理。</p>
<p>在许多情况下，实际控制设备的过程是非常复杂而且存在诸多细节。因此控制器的工作就是为操作系统提供一个更简单（但仍然非常复杂）的接口。也就是屏蔽物理细节。<strong>任何复杂的东西都可以加一层代理来解决，这是计算机或者人类社会很普世的一个解决方案</strong></p>
<p>I&#x2F;O 设备另一部分是设备本身，设备本身有一个相对简单的接口，这是因为接口既不能做很多工作，而且也已经被标准化了。例如，标准化后任何一个 SATA 磁盘控制器就可以适配任意一种 SATA 磁盘，所以标准化是必要的。<code>ATA</code> 代表 <code>高级技术附件(AT Attachment)</code>，而 SATA 表示<code>串行高级技术附件(Serial ATA)</code>。</p>
<p>现在 SATA 是很多计算机的标准硬盘接口。由于实际的设备接口隐藏在控制器中，所以操作系统看到的是对控制器的接口，这个接口和设备接口有很大区别。</p>
<p>每种类型的设备控制器都是不同的，所以需要不同的软件进行控制。专门与控制器进行信息交流，发出命令处理指令接收响应的软件，称为 <code>设备驱动程序(device driver)</code>。每个控制器厂家都应该针对不同的操作系统提供不同的设备驱动程序。</p>
<p>为了使设备驱动程序能够工作，必须把它安装在操作系统中，这样能够使它在内核态中运行。</p>
<p>每个设备控制器都有少量用于通信的寄存器，例如，一个最小的磁盘控制器也会有用于指定磁盘地址、内存地址、扇区计数的寄存器。要激活控制器，设备驱动程序会从操作系统获取一条指令，然后翻译成对应的值，并写入设备寄存器中，所有设备寄存器的结合构成了 <code>I/O 端口空间</code> 。</p>
<ul>
<li><p>在最简单的方式中，用户程序会发起系统调用，内核会将其转换为相应驱动程序的程序调用，然后设备驱动程序启动 I&#x2F;O 并循环检查该设备，看该设备是否完成了工作（一般会有一些二进制位用来指示设备仍在忙碌中）。当 I&#x2F;O 调用完成后，设备驱动程序把数据送到指定的地方并返回。然后操作系统会将控制权交给调用者。这种方式称为 <code>忙等待(busy waiting)</code>，这种方式的缺点是要一直占据 CPU，CPU 会一直轮询 I&#x2F;O 设备直到 I&#x2F;O 操作完成。</p>
</li>
<li><p>第二种方式是设备驱动程序启动设备并且让该设备在操作完成时发生中断。设备驱动程序在这个时刻返回。操作系统接着在需要时阻塞调用者并安排其他工作进行。当设备驱动程序检测到该设备操作完成时，它发出一个 <code>中断</code> 通知操作完成。</p>
<p><img src="https://pic.downk.cc/item/5f0591ad14195aa59409cb8f.png"></p>
<p>如上图所示，这是一个三步的 I&#x2F;O 过程，第一步，设备驱动程序会通过写入设备寄存器告诉控制器应该做什么。然后，控制器启动设备。当控制器完成读取或写入被告知需要传输的字节后，它会在步骤 2 中使用某些总线向中断控制器发送信号。如果中断控制器准备好了接收中断信号（如果正忙于一个优先级较高的中断，则可能不会接收），那么它就会在 CPU 的一个引脚上面声明。这就是步骤3</p>
<p>在第四步中，中断控制器把该设备的编号放在总线上，这样 CPU 可以读取总线，并且知道哪个设备完成了操作（可能同时有多个设备同时运行）。</p>
<p>一旦 CPU 决定去实施中断后，程序计数器和 PSW 就会被压入到当前堆栈中并且 CPU 会切换到内核态。设备编号可以作为内存的一个引用，用来寻找该设备中断处理程序的地址。这部分内存称作<code>中断向量(interrupt vector)</code>。一旦中断处理程序（中断设备的设备驱动程序的一部分）开始后，它会移除栈中的程序计数器和 PSW 寄存器，并把它们进行保存，然后查询设备的状态。在中断处理程序全部完成后，它会返回到先前用户程序尚未执行的第一条指令</p>
</li>
<li><p>实现 I&#x2F;O 的第三种方式是使用特殊的硬件：<code>直接存储器访问(Direct Memory Access, DMA)</code> 芯片。它可以控制内存和某些控制器之间的位流，而无需 CPU 的干预。CPU 会对 DMA 芯片进行设置，说明需要传送的字节数，有关的设备和内存地址以及操作方向。当 DMA 芯片完成后，会造成中断，中断过程就像上面描述的那样。我们会在后面具体讨论中断过程</p>
</li>
</ul>
<p>当另一个中断处理程序正在运行时，中断可能（并且经常）发生在不合宜的时间。因此，CPU 可以禁用中断，并且可以在之后重启中断。在 CPU 关闭中断后，任何已经发出中断的设备，可以继续保持其中断信号处理，但是 CPU 不会中断，直至中断再次启用为止。如果在关闭中断时，已经有多个设备发出了中断信号，中断控制器将决定优先处理哪个中断，通常这取决于事先赋予每个设备的优先级，最高优先级的设备优先赢得中断权，其他设备则必须等待。</p>
<h2 id="总线"><a href="#总线" class="headerlink" title="总线"></a>总线</h2><p>随着处理器核内存变得越来越快，单个总线处理所有请求的能力也达到了上线，其中也包括 IBM PC 总线。必须放弃使用这种模式。其结果导致了其他总线的出现，它们处理 I&#x2F;O 设备以及 CPU 到存储器的速度都更快。这种演变的结果导致了下面这种结构的出现。</p>
<p><img src="https://pic.downk.cc/item/5f05931814195aa5940a66bc.jpg"></p>
<p>上图中的 x86 系统包含很多总线，<strong>高速缓存、内存、PCIe、PCI、USB、SATA 和 DMI</strong>，每条总线都有不同的传输速率和功能。操作系统必须了解所有的总线配置和管理。其中最主要的总线是 <code>PCIe(Peripheral Component Interconnect Express)</code> 总线。</p>
<p>Intel 发明的 PCIe 总线也是作为之前古老的 PCI 总线的继承者，而古老的 PCI 总线也是为了取代古董级别的 <code>ISA(Industry Standard Architecture)</code> 总线而设立的。数十 Gb&#x2F;s 的传输能力使得 PCIe 比它的前身快很多，而且它们本质上也十分不同。直到发明 PCIe 的 2004 年，大多数总线都是并行且共享的。<code>共享总线架构(shared bus architeture)</code> 表示多个设备使用一些相同的电线传输数据。因此，当多个设备同时发送数据时，此时你需要一个决策者来决定谁能够使用总线。而 PCIe 则不一样，它使用专门的端到端链路。传统 PCI 中使用的<code>并行总线架构(parallel bus architecture)</code> 表示通过多条电线发送相同的数据字。例如，在传统的 PCI 总线上，一个 32 位数据通过 32 条并行的电线发送。而 PCIe 则不同，它选用了<code>串行总线架构(serial bus architecture)</code> ，并通过单个连接（称为通道）发送消息中的所有比特数据，就像网络数据包一样。这样做会简化很多，因为不再确保所有 32 位数据在同一时刻准确到达相同的目的地。通过将多个数据通路并行起来，并行性仍可以有效利用。例如，可以使用 32 条数据通道并行传输 32 条消息。</p>
<p>在上图结构中，CPU 通过 DDR3 总线与内存对话，通过 PCIe 总线与外围图形设备 （GPU）对话，通过 <code>DMI(Direct Media Interface)</code>总线经集成中心与所有其他设备对话。而集成控制中心通过串行总线与 USB 设备对话，通过 SATA 总线与硬盘和 DVD 驱动器对话，通过 PCIe 传输以太网络帧。</p>
<h2 id="计算机启动过程"><a href="#计算机启动过程" class="headerlink" title="计算机启动过程"></a>计算机启动过程</h2><p>那么有了上面一些硬件再加上操作系统的支持，我们的计算机就可以开始工作了，那么计算机的启动过程是怎样的呢？下面只是一个简要版的启动过程</p>
<p>在每台计算机上有一块双亲板，也就是母板，母板也就是主板，它是计算机最基本也就是最重要的部件之一。主板一般为矩形电路板，上面安装了组成计算机的主要电路系统，一般有 BIOS 芯片、I&#x2F;O 控制芯片、键盘和面板控制开关接口、指示灯插接件、扩充插槽、主板及插卡的直流电源供电接插件等元件。</p>
<p>在母板上有一个称为 <code>基本输入输出系统(Basic Input Output System, BIOS)</code>的程序。在 BIOS 内有底层 I&#x2F;O 软件，包括读键盘、写屏幕、磁盘I&#x2F;O 以及其他过程。如今，它被保存在闪存中，它是非易失性的，但是当BIOS 中发现错误时，可以由操作系统进行更新。</p>
<p>在计算机<code>启动(booted)</code>时，BIOS 开启，它会首先检查所安装的 RAM 的数量，键盘和其他基础设备是否已安装并且正常响应。接着，它开始扫描 PCIe 和 PCI 总线并找出连在上面的所有设备。即插即用的设备也会被记录下来。如果现有的设备和系统上一次启动时的设备不同，则新的设备将被重新配置。</p>
<p>然后，BIOS 通过尝试存储在 <code>CMOS</code> 存储器中的设备清单尝试启动设备</p>
<p>用户可以在系统启动后进入一个 BIOS 配置程序，对设备清单进行修改。然后，判断是否能够从外部 <code>CD-ROM</code> 和 USB 驱动程序启动，如果启动失败的话（也就是没有），系统将从硬盘启动，boots 设备中的第一个扇区被读入内存并执行。该扇区包含一个程序，该程序通常在引导扇区末尾检查分区表以确定哪个分区处于活动状态。然后从该分区读入第二个启动加载程序，该加载器从活动分区中读取操作系统并启动它。</p>
<p>然后操作系统会询问 BIOS 获取配置信息。对于每个设备来说，会检查是否有设备驱动程序。如果没有，则会向用户询问是否需要插入 <code>CD-ROM</code> 驱动（由设备制造商提供）或者从 Internet 上下载。一旦有了设备驱动程序，操作系统会把它们加载到内核中，然后初始化表，创建所需的后台进程，并启动登录程序或GUI。</p>
<h1 id="操作系统概念"><a href="#操作系统概念" class="headerlink" title="操作系统概念"></a>操作系统概念</h1><p>想象一下，没有操作系统，我们要通过CPU完成工作需要做什么事情？</p>
<p>CPU只知道两件事</p>
<p>1、从寄存器PC取指令2、执行指令。</p>
<p>指令从哪开始？指令从我们编写的程序根据编译器编译成机器语言。</p>
<p>指令怎么跳转？一般来说pc是自动加一，当遇到if或者跳转等指令，CPU在执行时根据计算结果动态改变PC的值。</p>
<p>如果没有操作系统，我们需要：</p>
<p>1、在内存中找一块大小合适区域装入程序2、找到函数入口，把地址传给PC</p>
<h2 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h2><p>机器指令需要加载到内存中执行，因此需要记录下内存的起始地址和长度；同时要找到函数的入口地址并写到PC寄存器中，想一想这是不是需要一个数据结构来记录下这些信息。</p>
<p><strong>数据结构大致如下：</strong></p>
<blockquote>
<p>struct *** {<br>   void* start_addr;<br>   intlen;<br>   void* start_point;<br>   …<br>};</p>
</blockquote>
<p>接下来就是起名字时刻。</p>
<p>这个数据结构总要有个名字吧，这个结构体用来记录什么信息呢？记录的是程序在被加载到内存中的运行状态，程序从磁盘加载到内存跑起来叫什么好呢？干脆就叫进程（Process）好了，我们的指导原则就是一定要听上去比较神秘，总之大家都不容易弄懂就对了，我将其称为“弄不懂原则”。</p>
<p>就这样进程诞生了。</p>
<hr>
<p>进程的缺点在于只有一个入口函数，也就是main函数，因此进程中的机器指令只能被一个CPU执行，那么有没有办法让多个CPU来执行同一个进程中的机器指令呢？</p>
<p>聪明的你应该能想到，既然我们可以把main函数的第一条指令地址写入PC寄存器，那么其它函数和main函数又有什么区别呢？</p>
<p>答案是没什么区别，main函数的特殊之处无非就在于是CPU执行的第一个函数，除此之外再无特别之处，我们可以把PC寄存器指向main函数，就可以把PC寄存器指向任何一个函数。</p>
<p>当我们把PC寄存器指向非main函数时，线程就诞生了。</p>
<p>值得注意的是，有了线程这个概念后，我们只需要进程开启后创建多个线程就可以让所有CPU都忙起来，这就是所谓高性能、高并发的根本所在。另外值得注意的一点是：由于各个线程共享进程的内存地址空间，因此线程之间的通信无需借助操作系统，这给程序员带来极大方便的同时也带来了无尽的麻烦，多线程遇到的多数问题都出自于线程间通信简直太方便了以至于非常容易出错。出错的根源在于CPU执行指令时根本没有线程的概念，多线程编程面临的互斥与同步问题需要程序员自己解决。</p>
<p><strong>无论使用任何编程语言，创建一个线程大体相同：</strong></p>
<blockquote>
<p>&#x2F;&#x2F; 设置线程入口函数DoSomething<br>thread &#x3D; CreateThread(DoSomething);<br>&#x2F;&#x2F; 让线程运行起来<br>thread.Run();</p>
</blockquote>
<p><strong>一般来说提交给线程池的任务包含两部分：</strong></p>
<ul>
<li><ol>
<li>需要被处理的数据；</li>
</ol>
</li>
<li><ol start="2">
<li>处理数据的函数。</li>
</ol>
</li>
</ul>
<p><strong>伪码描述一下：</strong></p>
<blockquote>
<p>struct task {<br>    void* data;     &#x2F;&#x2F; 任务所携带的数据<br>    handler handle; &#x2F;&#x2F; 处理数据的方法<br>}</p>
</blockquote>
<p>（注意：你也可以把代码中的struct理解成class，也就是对象)</p>
<p>线程池中的线程会阻塞在队列上，当生产者向队列中写入数据后，线程池中的某个线程会被唤醒，该线程从队列中取出上述结构体(或者对象)，以结构体(或者对象)中的数据为参数并调用处理函数。</p>
<p><strong>伪码如下：</strong></p>
<blockquote>
<p>while(true) {<br>  struct task &#x3D; GetFromQueue(); &#x2F;&#x2F; 从队列中取出数据<br>  task-&gt;handle(task-&gt;data);     &#x2F;&#x2F; 处理数据<br>}</p>
</blockquote>
<p>以上就是线程池最核心的部分。</p>
<p>理解这些你就能明白线程池是如何工作的了。</p>
<p>作者：Jack Jiang<br>链接：<a target="_blank" rel="noopener" href="https://zhuanlan.zhihu.com/p/339016204">https://zhuanlan.zhihu.com/p/339016204</a><br>来源：知乎<br>著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。</p>
<h4 id="进程切换"><a href="#进程切换" class="headerlink" title="进程切换"></a>进程切换</h4><p>一个cpu要处理成千上万个进程，就存在阻塞队列和就绪队列来管理进程的切换，当进行I&#x2F;O读取时，速度很慢，cpu不可能等到读取完成，因此把进行读取的进程放入阻塞队列，因为现有的DMA技术，没有cpu的帮助，一样可以进行i&#x2F;o读取，从就绪队列中取出进程进行cpu处理。</p>
<h4 id="地址空间"><a href="#地址空间" class="headerlink" title="地址空间"></a>地址空间</h4><p>与每个进程相关的是<code>地址空间(address space)</code>，这是从某个最小值的存储位置(通常是零)到某个最大值的存储位置的列表。地址空间中存放有可执行程序，程序所需要的数据和它的栈。与每个进程相关的还有资源集，通常包括<code>寄存器(registers)</code>（寄存器一般包括<code>程序计数器(program counter)</code>和<code>堆栈指针(stack pointer)</code>）、打开文件的清单、突发的报警、有关的进程清单和其他需要执行程序的信息。你可以把进程看作是容纳运行一个程序所有信息的一个容器。</p>
<p>操作系统周期性的挂起一个进程然后启动运行另一个进程，这可能是由于过去一两秒钟程序用完了 CPU 分配的时间片，而 CPU 转而运行另外的程序。</p>
<p>在许多操作系统中，与一个进程有关的所有信息，除了该进程自身地址空间的内容以外，均存放在操作系统的一张表中，称为 <code>进程表(process table)</code>，进程表是数组或者链表结构，当前存在每个进程都要占据其中的一项。</p>
<p>所以，一个挂起的进程包括：进程的地址空间（往往称作<code>磁芯映像</code>， core image，纪念过去的磁芯存储器），以及对应的进程表项（其中包括寄存器以及稍后启动该进程所需要的许多其他信息）。</p>
<p>与进程管理有关的最关键的往往是决定着进程的创建和终止的系统调用。考虑一个典型的例子，有一个称为 <code>命令解释器(command interpreter)</code> 或 <code>shell</code> 的进程从终端上读取命令。此时，用户刚键入一条命令要求编译一个程序。shell 必须先创建一个新进程来执行编译程序，当编译程序结束时，它执行一个系统调用来终止自己的进程。</p>
<p>如果一个进程能够创建一个或多个进程（称为<code>子进程</code>），而且这些进程又可以创建子进程，则很容易找到进程数</p>
<p>合作完成某些作业的相关进程经常需要彼此通信来完成作业，这种通信称为<code>进程间通信(interprocess communication)</code>。我们在后面会探讨进程间通信。</p>
<p>有时，需要向一个正在运行的进程传递信息，而该进程并没有等待接收信息。例如，一个进程通过网络向另一台机器上的进程发送消息进行通信。为了保证一条消息或消息的应答不丢失。发送者要求它所在的操作系统在指定的若干秒后发送一个通知，这样如果对方尚未收到确认消息就可以进行重新发送。在设定该定时器后，程序可以继续做其他工作。</p>
<p>在限定的时间到达后，操作系统会向进程发送一个 <code>警告信号(alarm signal)</code>。这个信号引起该进程暂时挂起，无论该进程正在做什么，系统将其寄存器的值保存到堆栈中，并开始重新启动一个特殊的信号处理程，比如重新发送可能丢失的消息。这些信号是软件模拟的硬件中断，除了定时器到期之外，该信号可以通过各种原因产生。许多由硬件检测出来的陷阱，如执行了非法指令或使用了无效地址等，也被转换成该信号并交给这个进程。</p>
<p>系统管理器授权每个进程使用一个给定的 <code>UID(User IDentification)</code>。每个启动的进程都会有一个操作系统赋予的 UID，子进程拥有与父进程一样的 UID。用户可以是某个组的成员，每个组也有一个 <code>GID(Group IDentification)</code>。</p>
<p>在 UNIX 操作系统中，有一个 UID 是 <code>超级用户(superuser)</code>，或者 Windows 中的<code>管理员(administrator)</code>，它具有特殊的权利，可以违背一些保护规则。在大型系统中，只有系统管理员掌握着那些用户可以称为超级用户。</p>
<h2 id="地址空间-1"><a href="#地址空间-1" class="headerlink" title="地址空间"></a>地址空间</h2><p>每台计算机都有一些主存用来保存正在执行的程序。在一个非常简单的操作系统中，仅仅有一个应用程序运行在内存中。为了运行第二个应用程序，需要把第一个应用程序移除才能把第二个程序装入内存。</p>
<p>复杂一些的操作系统会允许多个应用程序同时装入内存中运行。为了防止应用程序之间相互干扰（包括操作系统），需要有某种保护机制。虽然此机制是在硬件中实现，但却是由操作系统控制的。</p>
<p>上述观点涉及对计算机主存的管理和保护。另一种同等重要并与存储器有关的内容是管理进程的地址空间。通常，每个进程有一些可以使用的地址集合，典型值从 0 开始直到某个最大值。一个进程可拥有的最大地址空间小于主存。在这种情况下，即使进程用完其地址空间，内存也会有足够的内存运行该进程。</p>
<p>但是，在许多 32 位或 64 位地址的计算机中，分别有 2^32 或 2^64 字节的地址空间。如果一个进程有比计算机拥有的主存还大的地址空间，而且该进程希望使用全部的内存，那该怎么处理？在早期的计算机中是无法处理的。但是现在有了一种<code>虚拟内存</code>的技术，正如前面讲到过的，操作系统可以把部分地址空间装入主存，部分留在磁盘上，并且在需要时来回交换它们。</p>
<h2 id="文件"><a href="#文件" class="headerlink" title="文件"></a>文件</h2><p>实际上所有的I&#x2F;O设备都被抽象为了文件这个概念，一切皆文件（Everything is File），磁盘、网络数据、终端，甚至进程间通信工具管道pipe等都被当做文件对待。所有的I&#x2F;O操作也都可以通过文件读写来实现。</p>
<p><strong>创建文件、删除文件、读文件和写文件</strong> 都需要系统调用。</p>
<p>在 UNIX 中，另一个重要的概念是 <code>特殊文件(special file)</code>。提供特殊文件是为了使 I&#x2F;O 设备看起来像文件一般。这样，就像使用系统调用读写文件一样，I&#x2F;O 设备也可以通过同样的系统调用进行读写。特殊文件有两种，一种是<code>块儿特殊文件(block special file)</code> 和 <code>字符特殊文件(character special file)</code>。块特殊文件指那些由可随机存取的块组成的设备，如磁盘等。比如打开一个块特殊文件，然后读取第4块，程序可以直接访问设备的第4块而不必考虑存放在该文件的文件系统结构。类似的，字符特殊文件用于打印机、调制解调起和其他接受或输出字符流的设备。按照惯例，特殊文件保存在 <code>/dev</code> 目录中。例如，&#x2F;dev&#x2F;lp 是打印机。</p>
<p>还有一种与进程和文件相关的特性是管道，<code>管道(pipe)</code> 是一种虚文件，他可以连接两个进程</p>
<p>如果 A 和 B 希望通过管道对话，他们必须提前设置管道。当进程 A 相对进程 B 发送数据时，它把数据写到管道上，相当于管道就是输出文件。这样，在 UNIX 中两个进程之间的通信就非常类似于普通文件的读写了。</p>
<p>操作文件最重要的是获取文件描述符，例如：</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> fd = open(file_name); <span class="comment">// 获取文件描述符</span></span><br><span class="line">read(fd, buff);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 网络通信，通过accept获取客户端的文件描述符</span></span><br><span class="line"><span class="type">int</span> conn_fd = accept(...);</span><br><span class="line"><span class="comment">//服务端做的处理函数</span></span><br><span class="line"><span class="keyword">if</span>(read(conn_fd, msg_buff) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">do_transfer(msg_buff);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>对于高并发，server可能会跟多个客户端进行通信，每一个链接都是一个阻塞式I&#x2F;O，传统我们可以对每一个链接开一个线程，但是大量创建销毁线程是会严重影响系统性能。</p>
<p>一种改进方法就是异步调用，也就是改阻塞型I&#x2F;O端口为非阻塞型。异步调用有两种机制，一种是通知机制：就是发送信号，例如Linux的signal；第二种就是回调机制，就是常说的callback。</p>
<p>还有一种方法是多路复用：</p>
<p><strong>所谓I&#x2F;O多路复用指的是这样一个过程：</strong></p>
<ul>
<li>1）我们拿到了一堆文件描述符（不管是网络相关的、还是磁盘文件相关等等，任何文件描述符都可以）；</li>
<li>2）通过调用某个函数告诉内核：“这个函数你先不要返回，你替我监视着这些描述符，当这堆文件描述符中有可以进行I&#x2F;O读写操作的时候你再返回”；</li>
<li>3）当调用的这个函数返回后我们就能知道哪些文件描述符可以进行I&#x2F;O操作了。</li>
</ul>
<p><strong>以Linux为例，有这样三种机制可以用来进行I&#x2F;O多路复用：</strong></p>
<ul>
<li>1）select；提交1024个以内的文件描述符集合给内核进行监控，当有文件描述符可以读写了通知应用程序，应用程序遍历1024个集合。</li>
<li>2）poll；扩展了1024个限制。</li>
<li>3）epoll。因为文件描述符集合的变化频率比较低，epoll通过epoll_ctrl只操作那些变化的文件描述符。同时共享用户态和内核态的内存，避免了拷贝的开销。同时，epoll充当了进程（应用程序）和内核唤醒的文件描述符的中间代理。</li>
</ul>
<h2 id="协程"><a href="#协程" class="headerlink" title="协程"></a>协程</h2><p>协程跟普通函数不同，普通函数只有一个返回点，协程有多个返回点。不同于return返回，当从协程返回主函数后，还可以继续执行下面的代码。像python里的yield调用。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">void func() &#123;</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;a&quot;</span>)</span><br><span class="line"><span class="keyword">yield</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;b&quot;</span>)</span><br><span class="line"><span class="keyword">yield</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;c&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">A</span>():</span><br><span class="line">co =func() <span class="comment"># 得到该协程</span></span><br><span class="line"><span class="built_in">next</span>(co) <span class="comment"># 调用协程</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;in function A&quot;</span>) <span class="comment"># do something</span></span><br><span class="line"><span class="built_in">next</span>(co) <span class="comment"># 再次调用该协程</span></span><br></pre></td></tr></table></figure>

<p>线程的调度是操作系统实现的，这些对程序员都不可见，而协程是在用户态实现的，对程序员可见。</p>
<h1 id="系统调用"><a href="#系统调用" class="headerlink" title="系统调用"></a>系统调用</h1><p>我们已经可以看到操作系统提供了两种功能：为用户提供应用程序抽象和管理计算机资源。对于大部分在应用程序和操作系统之间的交互主要是应用程序的抽象，例如创建、写入、读取和删除文件。计算机的资源管理对用户来说基本上是透明的。因此，用户程序和操作系统之间的接口主要是处理抽象。为了真正理解操作系统的行为，我们必须仔细的分析这个接口。</p>
<p>多数现代操作系统都有功能相同但是细节不同的系统调用，引发操作系统的调用依赖于计算机自身的机制，而且必须用汇编代码表达。<strong>任何单 CPU 计算机一次执行执行一条指令</strong>。如果一个进程在用户态下运行用户程序，例如从文件中读取数据。那么如果想要把控制权交给操作系统控制，那么必须执行一个异常指令或者系统调用指令。操作系统紧接着需要参数检查找出所需要的调用进程。然后执行系统调用，把控制权移交给系统调用下面的指令。大致来说，系统调用就像是执行了一个特殊的过程调用，但是只有<strong>系统调用能够进入内核态而过程调用则不能进入内核态</strong>。</p>
<p>为了能够了解具体的调用过程，下面我们以 <code>read</code> 方法为例来看一下调用过程。像上面提到的那样，会有三个参数，第一个参数是指定文件、第二个是指向缓冲区、第三个参数是给定需要读取的字节数。就像几乎所有系统调用一样，它通过使用与系统调用相同的名称来调用一个函数库，从而从C程序中调用：read。</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">count = read(fd,buffer,nbytes);</span><br></pre></td></tr></table></figure>

<p>系统调用在 count 中返回实际读出的字节数。这个值通常与 nbytes 相同，但也可能更小。比如在读过程中遇到了文件尾的情况。</p>
<p>如果系统调用不能执行，不管是因为无效的参数还是磁盘错误，count 的值都会被置成 -1，然后在全局变量 <code>errno</code> 中放入错误信号。程序应该进场检查系统调用的结果以了解是否出错。</p>
<p>系统调用是通过一系列的步骤实现的，为了更清楚的说明这个概念，我们还以 read 调用为例，在准备系统调用前，首先会把参数压入堆栈，如下所示</p>
<p><img src="https://pic.downk.cc/item/5f08301f14195aa594d47cb5.jpg"></p>
<p>C 和 C++ 编译器使用逆序（必须把第一个参数赋值给 printf(格式字符串)，放在堆栈的顶部）。第一个参数和第三个参数都是值调用，但是第二个参数通过引用传递，即传递的是缓冲区的地址（由 &amp; 指示），而不是缓冲的内容。然后是 C 调用系统库的 read 函数，这也是第四步。</p>
<p>在由汇编语言写成的库过程中，一般把系统调用的编号放在操作系统所期望的地方，如寄存器（第五步）。然后执行一个 <code>TRAP</code> 指令，将用户态切换到内核态，并在内核中的一个固定地址开始执行第六步。TRAP 指令实际上与过程调用指令非常相似，它们后面都跟随一个来自远处位置的指令，以及供以后使用的一个保存在栈中的返回地址。</p>
<p>TRAP 指令与过程调用指令存在两个方面的不同</p>
<ul>
<li>TRAP 指令会改变操作系统的状态，由用户态切换到内核态，而过程调用不改变模式</li>
<li>其次，TRAP 指令不能跳转到任意地址上。根据机器的体系结构，要么跳转到一个单固定地址上，或者指令中有一 8 位长的字段，它给定了内存中一张表格的索引，这张表格中含有跳转地址，然后跳转到指定地址上。</li>
</ul>
<p>跟随在 TRAP 指令后的内核代码开始检查系统调用编号，然后<code>dispatch</code>给正确的系统调用处理器，这通常是通过一张由系统调用编号所引用的、指向系统调用处理器的指针表来完成第七步。此时，系统调用处理器运行第八步，一旦系统调用处理器完成工作，控制权会根据 TRAP 指令后面的指令中返回给函数调用库第九步。这个过程接着以通常的过程调用返回的方式，返回到客户应用程序，这是第十步。然后调用完成后，操作系统还必须清除用户堆栈，然后增加<code>堆栈指针(increment stackpointer)</code>，用来清除调用 read 之前压入的参数。从而完成整个 read 调用过程。</p>
<p>在上面的第九步中我们说道，控制可能返回 TRAP 指令后面的指令，把控制权再移交给调用者这个过程中，系统调用会发生阻塞，从而避免应用程序继续执行。这么做是有原因的。例如，如果试图读键盘，此时并没有任何输入，那么调用者就必须被阻塞。在这种情形下，操作系统会检查是否有其他可以运行的进程。这样，当有用户输入 时候，进程会提醒操作系统，然后返回第 9 步继续运行。</p>
<p>下面，我们会列出一些常用的 <code>POSIX</code> 系统调用，POSIX 系统调用大概有 100 多个，它们之中最重要的一些调用见下表</p>
<p><strong>进程管理</strong></p>
<table>
<thead>
<tr>
<th align="left">调用</th>
<th align="left">说明</th>
</tr>
</thead>
<tbody><tr>
<td align="left">pid &#x3D; fork()</td>
<td align="left">创建与父进程相同的子进程</td>
</tr>
<tr>
<td align="left">pid &#x3D; waitpid(pid, &amp;statloc,options)</td>
<td align="left">等待一个子进程终止</td>
</tr>
<tr>
<td align="left">s &#x3D; execve(name,argv,environp)</td>
<td align="left">替换一个进程的核心映像</td>
</tr>
<tr>
<td align="left">exit(status)</td>
<td align="left">终止进程执行并返回状态</td>
</tr>
</tbody></table>
<p><strong>文件管理</strong></p>
<table>
<thead>
<tr>
<th align="left">调用</th>
<th align="left">说明</th>
</tr>
</thead>
<tbody><tr>
<td align="left">fd &#x3D; open(file, how,…)</td>
<td align="left">打开一个文件使用读、写</td>
</tr>
<tr>
<td align="left">s &#x3D; close(fd)</td>
<td align="left">关闭一个打开的文件</td>
</tr>
<tr>
<td align="left">n &#x3D; read(fd,buffer,nbytes)</td>
<td align="left">把数据从一个文件读到缓冲区中</td>
</tr>
<tr>
<td align="left">n &#x3D; write(fd,buffer,nbytes)</td>
<td align="left">把数据从缓冲区写到一个文件中</td>
</tr>
<tr>
<td align="left">position &#x3D; iseek(fd,offset,whence)</td>
<td align="left">移动文件指针</td>
</tr>
<tr>
<td align="left">s &#x3D; stat(name,&amp;buf)</td>
<td align="left">取得文件状态信息</td>
</tr>
</tbody></table>
<p><strong>目录和文件系统管理</strong></p>
<table>
<thead>
<tr>
<th align="left">调用</th>
<th align="left">说明</th>
</tr>
</thead>
<tbody><tr>
<td align="left">s &#x3D; mkdir(nname,mode)</td>
<td align="left">创建一个新目录</td>
</tr>
<tr>
<td align="left">s &#x3D; rmdir(name)</td>
<td align="left">删去一个空目录</td>
</tr>
<tr>
<td align="left">s &#x3D; link(name1,name2)</td>
<td align="left">创建一个新目录项 name2,并指向 name1</td>
</tr>
<tr>
<td align="left">s &#x3D; unlink(name)</td>
<td align="left">删去一个目录项</td>
</tr>
<tr>
<td align="left">s &#x3D; mount(special,name,flag)</td>
<td align="left">安装一个文件系统</td>
</tr>
<tr>
<td align="left">s &#x3D; umount(special)</td>
<td align="left">卸载一个文件系统</td>
</tr>
</tbody></table>
<p><strong>其他</strong></p>
<table>
<thead>
<tr>
<th align="left">调用</th>
<th align="left">说明</th>
</tr>
</thead>
<tbody><tr>
<td align="left">s &#x3D; chdir(dirname)</td>
<td align="left">改变工作目录</td>
</tr>
<tr>
<td align="left">s &#x3D; chmod(name,mode)</td>
<td align="left">修改一个文件的保护位</td>
</tr>
<tr>
<td align="left">s &#x3D; kill(pid, signal)</td>
<td align="left">发送信号给进程</td>
</tr>
<tr>
<td align="left">seconds &#x3D; time(&amp;seconds)</td>
<td align="left">获取从 1970 年1月1日至今的时间</td>
</tr>
</tbody></table>
<p>上面的系统调用参数中有一些公共部分，例如 pid 系统进程 id，fd 是文件描述符，n 是字节数，position 是在文件中的偏移量、seconds 是流逝时间。</p>
<p>从宏观角度上看，这些系统调所提供的服务确定了多数操作系统应该具有的功能，下面分别来对不同的系统调用进行解释</p>
<h2 id="用于进程管理的系统调用"><a href="#用于进程管理的系统调用" class="headerlink" title="用于进程管理的系统调用"></a>用于进程管理的系统调用</h2><p>在 UNIX 中，<code>fork</code> 是唯一可以在 POSIX 中创建进程的途径，它创建一个原有进程的副本，包括所有的文件描述符、寄存器等内容。在 fork 之后，原有进程以及副本（父与子）就分开了。在 fork 过程中，所有的变量都有相同的值，虽然父进程的数据通过复制给子进程，但是后续对其中任何一个进程的修改不会影响到另外一个。fork 调用会返回一个值，在子进程中该值为 0 ，并且在父进程中等于子进程的 <code>进程标识符(Process IDentified,PID)</code>。使用返回的 PID，就可以看出来哪个是父进程和子进程。</p>
<p>在多数情况下， 在 fork 之后，子进程需要执行和父进程不一样的代码。从终端读取命令，创建一个子进程，等待子进程执行命令，当子进程结束后再读取下一个输入的指令。为了等待子进程完成，父进程需要执行 <code>waitpid</code> 系统调用，父进程会等待直至子进程终止（若有多个子进程的话，则直至任何一个子进程终止）。waitpid 可以等待一个特定的子进程，或者通过将第一个参数设为 -1 的方式，等待任何一个比较老的子进程。当 waitpid 完成后，会将第二个参数 <code>statloc</code> 所指向的地址设置为子进程的退出状态（正常或异常终止以及退出值）。有各种可使用的选项，它们由第三个参数确定。例如，如果没有已经退出的子进程则立刻返回。</p>
<p>那么 shell 该如何使用 fork 呢？在键入一条命令后，shell 会调用 fork 命令创建一个新的进程。这个子进程会执行用户的指令。通过使用 <code>execve</code> 系统调用可以实现系统执行，这个系统调用会引起整个核心映像被一个文件所替代，该文件由第一个参数给定。下面是一个简化版的例子说明 fork、waitpid 和 execve 的使用</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">#define TRUE 1</span><br><span class="line"></span><br><span class="line">/* 一直循环下去 */</span><br><span class="line">while(TRUE)&#123;			</span><br><span class="line"></span><br><span class="line">/* 在屏幕上显示提示符 */</span><br><span class="line">	type_prompt();			</span><br><span class="line">  </span><br><span class="line">  /* 从终端读取输入 */</span><br><span class="line">	read_command(command,parameters)		</span><br><span class="line">  </span><br><span class="line">  /* fork 子进程 */</span><br><span class="line">	if(fork() != 0)&#123;								</span><br><span class="line">  </span><br><span class="line">		/* 父代码 */</span><br><span class="line">		/* 等待子进程执行完毕 */</span><br><span class="line">		waitpid(-1, &amp;status, 0);								</span><br><span class="line">	&#125;else&#123;</span><br><span class="line">	</span><br><span class="line">		/* 执行命令 */</span><br><span class="line">		/* 子代码 */</span><br><span class="line">		execve(command,parameters,0)					</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>一般情况下，execve 有三个参数：将要执行的文件名称，一个指向变量数组的指针，以及一个指向环境数组的指针。这里对这些参数做一个简要的说明。</p>
<p>先看一个 shell 指令</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cp file1 file2</span><br></pre></td></tr></table></figure>

<p>此命令把 file1 复制到 file2 文件中，在 shell 执行 fork 之后，子进程定位并执行文件拷贝，并将源文件和目标文件的名称传递给它。</p>
<p>cp 的主程序（以及包含其他大多数 C 程序的主程序）包含声明</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main(argc,argv,envp)</span><br></pre></td></tr></table></figure>

<p>其中 argc 是命令行中参数数目的计数，包括程序名称。对于上面的例子，<code>argc</code> 是3。第二个参数<code>argv</code> 是数组的指针。该数组的元素 i 是指向该命令行第 i 个字符串的指针。在上面的例子中，argv[0] 指向字符串 cp，argv[1] 指向字符串 file1，argv[2] 指向字符串 file2。main 的第三个参数是指向环境的指针，该环境是一个数组，含有 <code>name = value</code> 的赋值形式，用以将诸如终端类型以及根目录等信息传送给程序。这些变量通常用来确定用户希望如何完成特定的任务（例如，使用默认打印机）。在上面的例子中，没有环境参数传递给 execve ，所以环境变量是 0 ，所以 execve 的第三个参数为 0 。</p>
<p>可能你觉得 execve 过于复杂，这时候我要鼓励一下你，execve 可能是 POSIX 的全部系统调用中最复杂的一个了，其他都比较简单。作为一个简单的例子，我们再来看一下 <code>exit</code> ，这是进程在执行完成后应执行的系统调用。这个系统调用有一个参数，它的退出状态是 0 - 255 之间，它通过 waitpid 系统调用中的 statloc 返回给父级。</p>
<p>UNIX 中的进程将内存划分成三个部分：<code>text segment,文本区</code>，例如程序代码，<code>data segment，数据区</code>，例如变量，<code>stack segment</code>，栈区域。数据地址向上增长而堆栈向下增长，如下图所示</p>
<p><img src="https://pic.downk.cc/item/5f4cee45160a154a67f7da37.jpg"></p>
<p>上图能说明三个部分的内存分配情况，夹在中间的是空闲区，也就是未分配的区域，堆栈在需要时自动的挤压空闲区域，不过数据段的扩展是显示地通过系统调用 <code>brk</code> 进行的，在数据段扩充后，该系统调用指向一个新地址。但是，这个调用不是 POSIX 标准中定义的，对于存储器的动态分配，鼓励程序员使用 <code>malloc</code> 函数，而 malloc 的内部实现则不是一个适合标准化的主题，因为几乎没有程序员直接使用它。</p>
<h2 id="用于文件管理的系统调用"><a href="#用于文件管理的系统调用" class="headerlink" title="用于文件管理的系统调用"></a>用于文件管理的系统调用</h2><blockquote>
<p><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s?__biz=MzU2NDg0OTgyMA==&mid=2247485559&idx=1&sn=dfc9973ebf045284c771efa74cfc4ee7&chksm=fc45f584cb327c92bf4f5854c5dca343bc35a30bf385fa42b4d8d3f0d08ea48d59416dc9a7dc&token=1981249032&lang=zh_CN#rd">原文章地址</a></p>
</blockquote>
<h1 id="网络编程"><a href="#网络编程" class="headerlink" title="网络编程"></a>网络编程</h1><p>网络编程是基于client-server模型。</p>
<p>互联网络的关键是在每个主机和路由器上运行一个协议软件，这个软件基于统一的协议分配地址，并且在电缆上编码位和将位封装成帧。通过一种统一的将数据位捆扎成不连续的片（包）的统一方式，从而消除了差异。一个包是由包头和有效载荷组成，包头包括包的大小和源目的地址，有效载荷包括从源主机发出的数据位。</p>
<p><img src="https://pic.downk.cc/item/5f4cf328160a154a67fc1287.jpg"></p>
<p>2.注意协议软件对数据的封装是两层的，添加互联网包头（ip）和LAN1的帧头（MAC），包头负责寻找到主机B，LAN1帧头负责寻址到路由器，传输给LAN2适配器。LAN1帧的有效载荷是一个互联网包，互联网包的有效载荷是实际数据。</p>
<p>4.路由器接到LAN1适配器接受到的帧后，从包头里提取目的网络地址dst_ip，并将它作为路由表的索引确定向哪转发这个包，并且剥离LAN1的帧头，重新对互联网包进行封装，添加寻址到的LAN2帧头。</p>
<h3 id="套接字"><a href="#套接字" class="headerlink" title="套接字"></a>套接字</h3><p>一个套接字是连接的一个端点。每个套接字都有相应的套接字地址。用地址：端口表示。</p>
<p>当客户端发送一个网络请求时或者一个进程间请求（连接请求）。内核会临时为客户端的进程分配一个端口（临时端口），而服务器上提供服务的端口一般是默认通用的，例如ssh服务的22端口，web服务的80端口，邮件服务的25端口。</p>
<p>一个连接是由两端的套接字地址唯一确定的。</p>
<p>对于内核来说，套接字地址是一个端口，对于linux应用程序来说，套接字就是一个有相应描述符打开的文件，向文件传输或者接受数据。</p>
<h3 id="WEB服务器"><a href="#WEB服务器" class="headerlink" title="WEB服务器"></a>WEB服务器</h3><p>客户端和web服务器是基于超文本协议（http）传输数据。</p>
<p><b>http</b></p>
<p><a href='tkhfree.github.io'>互联网跳转指针连接</a></p>
<p>web服务器以两种方式向客户端提供内容：</p>
<p>静态内容：磁盘文件</p>
<p>动态内容：运行一个可执行文件</p>
<h4 id="web内容"><a href="#web内容" class="headerlink" title="web内容"></a>web内容</h4><p>MIME类型：</p>
<p>text&#x2F;html html页面</p>
<p>text&#x2F;plain 无格式文本</p>
<p>application&#x2F;postscript postscript文档</p>
<p>image&#x2F;gif 二进制图像</p>
<p>image&#x2F;png 二进制图像</p>
<p>image&#x2F;jpeg 二进制图像</p>
<h4 id="HTTP事务"><a href="#HTTP事务" class="headerlink" title="HTTP事务"></a>HTTP事务</h4><h5 id="HTTP请求"><a href="#HTTP请求" class="headerlink" title="HTTP请求"></a>HTTP请求</h5><p>http请求包括一个文本请求行，后面跟随0个或者多个请求报头，在跟随一个空的文本行终止包头列表</p>
<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GET / HTTP/1.1 #请求行</span><br><span class="line"><span class="attribute">Host</span><span class="punctuation">: </span>www.baidu.com #请求报头</span><br><span class="line">                    #空的</span><br></pre></td></tr></table></figure>

<p>请求行的形式是method URI version</p>
<p>method：GET DELETE POST OPTIONAL PUT HEADER</p>
<p>URI是URL的后缀，包括文件名和可选的参数</p>
<p>version是HTTP的版本，一般只有1.0和1.1，互相兼容</p>
<p>请求报头为服务器提供了额外的信息，例如浏览器的商标或者浏览器可以理解的MIME类型。请求报头格式：</p>
<p>header-name：header-data</p>
<h5 id="HTTP响应"><a href="#HTTP响应" class="headerlink" title="HTTP响应"></a>HTTP响应</h5><p>http响应包括一个响应行，跟随多个响应报头，再跟随一个终止报头的空行，在跟随响应主体</p>
<p>响应行格式是version status-code status-message</p>
<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">HTTP/1.0</span> <span class="number">200</span> ok #响应行</span><br><span class="line">MIME-VERSION1:1.0 #响应报头</span><br><span class="line">DATE:MON, 8 JAN 2000 05:05:05 GMT</span><br><span class="line">SERVER:APACHE-COyote/1.1</span><br><span class="line">Content-Type:text/html</span><br><span class="line">Content-Length:55555 #响应报头</span><br><span class="line">                     #空行，终止报头</span><br><span class="line">&lt;html&gt;               #响应主体，response body</span><br><span class="line">...</span><br><span class="line">&lt;/html&gt;</span><br></pre></td></tr></table></figure>

<h4 id="服务动态内容"><a href="#服务动态内容" class="headerlink" title="服务动态内容"></a>服务动态内容</h4><p>客户端如何将参数传递给服务器？服务器如何传递参数给它的子进程？服务器如何将子进程生成内容所需的其他信息传递给子进程？子进程将输出传送到哪里？</p>
<h5 id="CGI-通用网关接口common-gateway-interface"><a href="#CGI-通用网关接口common-gateway-interface" class="headerlink" title="CGI(通用网关接口common gateway interface)"></a>CGI(通用网关接口common gateway interface)</h5><ol>
<li><p>客户端将参数传递给服务器</p>
<p>GET请求参数放在URI里吗？分割路径和参数，&amp;分割参数</p>
<p>POST请求参数放在主体中，这样的请求会把参数隐藏起来</p>
</li>
<li><p>服务器将参数传递给子进程</p>
<p>服务器接受到 </p>
<p>GET  &#x2F;cgi-bin&#x2F;adder?15000&amp;213  HTTP&#x2F;1.1</p>
<p>之后，它调用fork创建子进程，并且执行execve在子进程的上下文中执行&#x2F;cgi-bin&#x2F;adder程序，在调用execve之前，子进程将CGI环境变量QUERY_STRING设置为15000&amp;213，adder是一个标准的遵循CGI格式的函数</p>
</li>
<li><p>服务器如何将其他信息传递给子进程</p>
<p>CGI程序可以在运行时设置环境变量</p>
</li>
<li><p>子进程将输出传送到哪里</p>
<p>CGI程序将他的输出发送到标准输出，在子进程加载并运行CGI之前，使用Linux dup2函数将标准输出重定向到和客户端相连接的已连接描述符，因此标准输出的的内容会直接传输到客户端</p>
<p>子进程负责生成content-type和content-length响应报头以及终止报头的空行</p>
</li>
</ol>
<h1 id="链接"><a href="#链接" class="headerlink" title="链接"></a>链接</h1><p>链接是将各种代码和数据片段收集组合成一个单一文件的过程，输入是各种数据和代码块，输出是可执行文件（a.out)。</p>
<p>预处理器、编译器、汇编器把源文件（.c）生成数据和代码块</p>
<p>链接器生成可执行文件</p>
<p>shell调用一个加载器的函数执行</p>
<h3 id="链接器的任务"><a href="#链接器的任务" class="headerlink" title="链接器的任务"></a>链接器的任务</h3><ul>
<li>链接器是把在一个数据块定义的符号和在另一个代码块引用的符号对应上。</li>
<li>另一个作用是在内存位置的重定向，因为编译器和汇编器把代码和数据块都是从地址0开始生成的，而实际运行时肯定是在内存中某一个非0位置，因此链接器通过将每一个符号与内存位置关联起来重定位这些节，然后修改所有对这些符号的引用，指向这个内存的具体位置。</li>
</ul>
<h4 id="目标文件"><a href="#目标文件" class="headerlink" title="目标文件"></a>目标文件</h4><ul>
<li>可重定位的目标文件    main.o</li>
<li>可执行的文件    a.out</li>
<li>共享目标文件   系统库libc.o</li>
</ul>
<p>可重定位的目标文件一般就是自己编译器生成的目标文件，编译成可执行文件</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gcc -o main main.o xxx.o</span><br></pre></td></tr></table></figure>

<p>一般来说我们会讲一些标准函数打包在一起，让调用的函数可以直接链接，不用自己再单独一个个链接，例如libc.o libm.o，这样对于编译器和标准函数进行分离，但是有一个缺点：</p>
<p><strong>每一个程序都需要拷贝编译进来的标准函数，当程序很多时，每个程序都有一个标准函数的副本，造成磁盘空间的浪费，运行的时候都放在内存中，即使两个程序链接了同一个标准函数，内存中一样有两份。而且标准函数进行更改后，所有的程序都需要重新编译一遍</strong></p>
<p>为了方便，将所有的标准函数编译成独立的目标文件，例如printf函数编译成printf.o、scanf函数编译成scanf.o，然后将它们打包放进一个标准库里，编译时直接添加这个库名字，就会把引用到的标准函数的目标文件编译进去，减少了每次都要复制一次目标文件（因为这些目标文件放在固定位置），减少了编译时输入很多目标文件的名字（因为被打包成库）</p>
<p><strong>命令行上的库和目标文件的顺序是很重要的，一般将库放在命令行的结尾</strong></p>
<h4 id="重定位"><a href="#重定位" class="headerlink" title="重定位"></a>重定位</h4><p>主要依赖重定位条目</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span>&#123;</span></span><br><span class="line">    xx</span><br><span class="line">    xx</span><br><span class="line">&#125;Elf64_Rela;</span><br></pre></td></tr></table></figure>

<h1 id="进程-1"><a href="#进程-1" class="headerlink" title="进程"></a>进程</h1><p>操作系统中最核心的概念就是 <code>进程</code>，进程是对正在运行中的程序的一个抽象。操作系统的其他所有内容都是围绕着进程展开的。进程是操作系统提供的最古老也是最重要的概念之一。即使可以使用的 CPU 只有一个，它们也支持<code>（伪）并发</code>操作。它们会将一个单独的 CPU 抽象为多个虚拟机的 CPU。可以说：没有进程的抽象，现代操作系统将不复存在。</p>
<p>所有现代的计算机会在同一时刻做很多事情，过去使用计算机的人（单 CPU）可能完全无法理解现在这种变化，举个例子更能说明这一点：首先考虑一个 Web 服务器，请求都来自于 Web 网页。当一个请求到达时，服务器会检查当前页是否在缓存中，如果是在缓存中，就直接把缓存中的内容返回。如果缓存中没有的话，那么请求就会交给磁盘来处理。但是，从 CPU 的角度来看，磁盘请求需要更长的时间，因为磁盘请求会很慢。当硬盘请求完成时，更多其他请求才会进入。如果有多个磁盘的话，可以在第一个请求完成前就可以连续的对其他磁盘发出部分或全部请求。很显然，这是一种并发现象，需要有并发控制条件来控制并发现象。</p>
<p>在许多多道程序系统中，CPU 会在<code>进程</code>间快速切换，使每个程序运行几十或者几百毫秒。然而，严格意义来说，在某一个瞬间，CPU 只能运行一个进程，然而我们如果把时间定位为 1 秒内的话，它可能运行多个进程。这样就会让我们产生<code>并行</code>的错觉。有时候人们说的 <code>伪并行(pseudoparallelism)</code> 就是这种情况，以此来区分多处理器系统(该系统由两个或多个 CPU 来共享同一个物理内存)</p>
<p>因为 CPU 执行速度很快，进程间的换进换出也非常迅速，因此我们很难对多个并行进程进行跟踪，所以，在经过多年的努力后，操作系统的设计者开发了用于描述并行的一种概念模型（顺序进程），使得并行更加容易理解和分析，对该模型的探讨，也是本篇文章的主题。下面我们就来探讨一下进程模型</p>
<h2 id="进程模型"><a href="#进程模型" class="headerlink" title="进程模型"></a>进程模型</h2><p>在进程模型中，所有计算机上运行的软件，通常也包括操作系统，被组织为若干<code>顺序进程(sequential processes)</code>，简称为 <code>进程(process)</code> 。一个进程就是一个正在执行的程序的实例，进程也包括程序计数器、寄存器和变量的当前值。从概念上来说，每个进程都有各自的虚拟 CPU，但是实际情况是 CPU 会在各个进程之间进行来回切换。</p>
<p>由于 CPU 会在各个进程之间来回快速切换，所以每个进程在 CPU 中的运行时间是无法确定的。并且当同一个进程再次在 CPU 中运行时，其在 CPU 内部的运行时间往往也是不固定的。进程和程序之间的区别是非常微妙的，但是通过一个例子可以让你加以区分：想想一位会做饭的计算机科学家正在为他的女儿制作生日蛋糕。他有做生日蛋糕的食谱，厨房里有所需的原谅：面粉、鸡蛋、糖、香草汁等。在这个比喻中，做蛋糕的食谱就是程序、计算机科学家就是 CPU、而做蛋糕的各种原料都是输入数据。进程就是科学家阅读食谱、取来各种原料以及烘焙蛋糕等一系列动作的总和。</p>
<p>现在假设科学家的儿子跑过来告诉他，说他的头被蜜蜂蜇了一下，那么此时科学家会记录出来他做蛋糕这个过程到了哪一步，然后拿出急救手册，按照上面的步骤给他儿子实施救助。这里，会涉及到进程之间的切换，科学家（CPU）会从做蛋糕（进程）切换到实施医疗救助（另一个进程）。等待伤口处理完毕后，科学家会回到刚刚记录做蛋糕的那一步，继续制作。</p>
<p>这里的关键思想是<code>认识到一个进程所需的条件</code>，进程是某一类特定活动的总和，它有程序、输入输出以及状态。单个处理器可以被若干进程共享，它使用某种调度算法决定何时停止一个进程的工作，并转而为另外一个进程提供服务。另外需要注意的是，如果一个进程运行了两遍，则被认为是两个进程。那么我们了解到进程模型后，那么进程是如何创建的呢？</p>
<h2 id="进程的创建"><a href="#进程的创建" class="headerlink" title="进程的创建"></a>进程的创建</h2><p>操作系统需要一些方式来创建进程。下面是一些创建进程的方式</p>
<ul>
<li>系统初始化（init）</li>
<li>正在运行的程序执行了创建进程的系统调用（比如 fork）</li>
<li>用户请求创建一个新进程</li>
<li>初始化一个批处理工作</li>
</ul>
<h3 id="系统初始化"><a href="#系统初始化" class="headerlink" title="系统初始化"></a>系统初始化</h3><p>启动操作系统时，通常会创建若干个进程。其中有些是<code>前台进程(numerous processes)</code>，也就是同用户进行交互并替他们完成工作的进程。一些运行在后台，并不与特定的用户进行交互，例如，设计一个进程来接收发来的电子邮件，这个进程大部分的时间都在休眠，但是只要邮件到来后这个进程就会被唤醒。还可以设计一个进程来接收对该计算机上网页的传入请求，在请求到达的进程唤醒来处理网页的传入请求。进程运行在后台用来处理一些活动像是 e-mail，web 网页，新闻，打印等等被称为 <code>守护进程(daemons)</code>。大型系统会有很多守护进程。在 UNIX 中，<code>ps</code> 程序可以列出正在运行的进程， 在 Windows 中，可以使用任务管理器。</p>
<h3 id="系统调用创建"><a href="#系统调用创建" class="headerlink" title="系统调用创建"></a>系统调用创建</h3><p>除了在启动阶段创建进程之外，一些新的进程也可以在后面创建。通常，一个正在运行的进程会发出<code>系统调用</code>用来创建一个或多个新进程来帮助其完成工作。例如，如果有大量的数据需要经过网络调取并进行顺序处理，那么创建一个进程读数据，并把数据放到共享缓冲区中，而让第二个进程取走并正确处理会比较容易些。在多处理器中，让每个进程运行在不同的 CPU 上也可以使工作做的更快。</p>
<h3 id="用户请求创建"><a href="#用户请求创建" class="headerlink" title="用户请求创建"></a>用户请求创建</h3><p>在许多交互式系统中，输入一个命令或者双击图标就可以启动程序，以上任意一种操作都可以选择开启一个新的进程，在基本的 UNIX 系统中运行 X，新进程将接管启动它的窗口。在 Windows 中启动进程时，它一般没有窗口，但是它可以创建一个或多个窗口。每个窗口都可以运行进程。通过鼠标或者命令就可以切换窗口并与进程进行交互。</p>
<h3 id="批处理创建"><a href="#批处理创建" class="headerlink" title="批处理创建"></a>批处理创建</h3><p>最后一种创建进程的情形会在<code>大型机的批处理系统</code>中应用。用户在这种系统中提交批处理作业。当操作系统决定它有资源来运行另一个任务时，它将创建一个新进程并从其中的输入队列中运行下一个作业。</p>
<p>从技术上讲，在所有这些情况下，让现有流程执行流程是通过创建系统调用来创建新流程的。该进程可能是正在运行的用户进程，是从键盘或鼠标调用的系统进程或批处理程序。这些就是系统调用创建新进程的过程。该系统调用告诉操作系统创建一个新进程，并直接或间接指示在其中运行哪个程序。</p>
<p>在 UNIX 中，仅有一个系统调用来创建一个新的进程，这个系统调用就是 <code>fork</code>。这个调用会创建一个与调用进程相关的副本。在 fork 后，一个父进程和子进程会有相同的内存映像，相同的环境字符串和相同的打开文件。通常，子进程会执行 <code>execve</code> 或者一个简单的系统调用来改变内存映像并运行一个新的程序。例如，当一个用户在 shell 中输出 sort 命令时，shell 会 fork 一个子进程然后子进程去执行 sort 命令。这两步过程的原因是允许子进程在 fork 之后但在 execve 之前操作其文件描述符，以完成标准输入，标准输出和标准错误的重定向。</p>
<p>在 Windows 中，情况正相反，一个简单的 Win32 功能调用 <code>CreateProcess</code>，会处理流程创建并将正确的程序加载到新的进程中。这个调用会有 10 个参数，包括了需要执行的程序、输入给程序的命令行参数、各种安全属性、有关打开的文件是否继承控制位、优先级信息、进程所需要创建的窗口规格以及指向一个结构的指针，在该结构中新创建进程的信息被返回给调用者。除了 <code>CreateProcess</code> Win 32 中大概有 100 个其他的函数用于处理进程的管理，同步以及相关的事务。</p>
<p>在 UNIX 和 Windows 中，进程创建之后，父进程和子进程有各自不同的地址空间。如果其中某个进程在其地址空间中修改了一个词，这个修改将对另一个进程不可见。在 UNIX 中，子进程的地址空间是父进程的一个拷贝，但是却是两个不同的地址空间；不可写的内存区域是共享的。某些 UNIX 实现是正是在两者之间共享，因为它不能被修改。或者，子进程共享父进程的所有内存，但是这种情况下内存通过 <code>写时复制(copy-on-write)</code> 共享，这意味着一旦两者之一想要修改部分内存，则这块内存首先被明确的复制，以确保修改发生在私有内存区域。再次强调，<strong>可写的内存是不能被共享的</strong>。但是，对于一个新创建的进程来说，确实有可能共享创建者的资源，比如可以共享打开的文件。<strong>在 Windows 中，从一开始父进程的地址空间和子进程的地址空间就是不同的</strong>。</p>
<h2 id="进程的终止"><a href="#进程的终止" class="headerlink" title="进程的终止"></a>进程的终止</h2><p>进程在创建之后，它就开始运行并做完成任务。然而，没有什么事儿是永不停歇的，包括进程也一样。进程早晚会发生终止，但是通常是由于以下情况触发的</p>
<ul>
<li><code>正常退出(自愿的)</code></li>
<li><code>错误退出(自愿的)</code></li>
<li><code>严重错误(非自愿的)</code></li>
<li><code>被其他进程杀死(非自愿的)</code></li>
</ul>
<h3 id="正常退出"><a href="#正常退出" class="headerlink" title="正常退出"></a>正常退出</h3><p>多数进程是由于完成了工作而终止。当编译器完成了所给定程序的编译之后，编译器会执行一个系统调用告诉操作系统它完成了工作。这个调用在 UNIX 中是 <code>exit</code> ，在 Windows 中是 <code>ExitProcess</code>。面向屏幕中的软件也支持自愿终止操作。字处理软件、Internet 浏览器和类似的程序中总有一个供用户点击的图标或菜单项，用来通知进程删除它所打开的任何临时文件，然后终止。</p>
<h3 id="错误退出"><a href="#错误退出" class="headerlink" title="错误退出"></a>错误退出</h3><p>进程发生终止的第二个原因是发现严重错误，例如，如果用户执行如下命令</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cc foo.c		</span><br></pre></td></tr></table></figure>

<p>为了能够编译 foo.c 但是该文件不存在，于是编译器就会发出声明并退出。在给出了错误参数时，面向屏幕的交互式进程通常并不会直接退出，因为这从用户的角度来说并不合理，用户需要知道发生了什么并想要进行重试，所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误，是需要重试还是退出。</p>
<h3 id="严重错误"><a href="#严重错误" class="headerlink" title="严重错误"></a>严重错误</h3><p>进程终止的第三个原因是由进程引起的错误，通常是由于程序中的错误所导致的。例如，执行了一条非法指令，引用不存在的内存，或者除数是 0 等。在有些系统比如 UNIX 中，进程可以通知操作系统，它希望自行处理某种类型的错误，在这类错误中，进程会收到信号（中断），而不是在这类错误出现时直接终止进程。</p>
<h3 id="被其他进程杀死"><a href="#被其他进程杀死" class="headerlink" title="被其他进程杀死"></a>被其他进程杀死</h3><p>第四个终止进程的原因是，某个进程执行系统调用告诉操作系统杀死某个进程。在 UNIX 中，这个系统调用是 kill。在 Win32 中对应的函数是 <code>TerminateProcess</code>（注意不是系统调用）。</p>
<h2 id="进程的层次结构"><a href="#进程的层次结构" class="headerlink" title="进程的层次结构"></a>进程的层次结构</h2><p>在一些系统中，当一个进程创建了其他进程后，父进程和子进程就会以某种方式进行关联。子进程它自己就会创建更多进程，从而形成一个进程层次结构。</p>
<h3 id="UNIX-进程体系"><a href="#UNIX-进程体系" class="headerlink" title="UNIX 进程体系"></a>UNIX 进程体系</h3><p>在 UNIX 中，进程和它的所有子进程以及子进程的子进程共同组成一个进程组。当用户从键盘中发出一个信号后，该信号被发送给当前与键盘相关的进程组中的所有成员（它们通常是在当前窗口创建的所有活动进程）。每个进程可以分别捕获该信号、忽略该信号或采取默认的动作，即被信号 kill 掉</p>
<p>这里有另一个例子，可以用来说明层次的作用，考虑 UNIX 在启动时如何初始化自己。一个称为 init 的特殊进程出现在启动映像中 。当 init 进程开始运行时，它会读取一个文件，文件会告诉它有多少个终端。然后为每个终端创建一个新进程。这些进程等待用户登录。如果登录成功，该登录进程就执行一个 shell 来等待接收用户输入指令，这些命令可能会启动更多的进程，以此类推。因此，整个操作系统中所有的进程都隶属于一个单个以 init 为根的进程树。</p>
<h3 id="Windows-进程体系"><a href="#Windows-进程体系" class="headerlink" title="Windows 进程体系"></a>Windows 进程体系</h3><p>相反，Windows 中没有进程层次的概念，Windows 中所有进程都是平等的，唯一类似于层次结构的是在创建进程的时候，父进程得到一个特别的令牌（称为句柄），该句柄可以用来控制子进程。然而，这个令牌可能也会移交给别的操作系统，这样就不存在层次结构了。而在 UNIX 中，进程不能剥夺其子进程的 <code>进程权</code>。（这样看来，还是 Windows 比较<code>渣</code>）。</p>
<h2 id="进程状态"><a href="#进程状态" class="headerlink" title="进程状态"></a>进程状态</h2><p>尽管每个进程是一个独立的实体，有其自己的程序计数器和内部状态，但是，进程之间仍然需要相互帮助。例如，一个进程的结果可以作为另一个进程的输入，在 shell 命令中</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat file1 file2 file3 | grep tree</span><br></pre></td></tr></table></figure>

<p>一个进程是 <code>cat</code>，将三个文件级联并输出。第二个进程是 <code>grep</code>，它从输入中选择具有包含关键字 <code>tree</code> 的内容，根据这两个进程的相对速度（这取决于两个程序的相对复杂度和各自所分配到的 CPU 时间片），可能会发生下面这种情况，<code>grep</code> 准备就绪开始运行，但是输入进程还没有完成，于是必须阻塞 grep 进程，直到输入完毕。</p>
<p>当一个进程开始运行时，它可能会经历下面这几种状态</p>
<p><img src="https://pic.downk.cc/item/5f07df1214195aa594bd00e0.jpg"></p>
<p>图中会涉及三种状态</p>
<ol>
<li><code>运行态</code>，运行态指的就是进程实际占用 CPU 时间片运行时</li>
<li><code>就绪态</code>，就绪态指的是可运行，但因为其他进程正在运行而处于就绪状态</li>
<li><code>阻塞态</code>，除非某种外部事件发生，否则进程不能运行</li>
</ol>
<p>逻辑上来说，运行态和就绪态是很相似的。这两种情况下都表示进程<code>可运行</code>，但是第二种情况没有获得 CPU 时间分片。第三种状态与前两种状态不同的原因是这个进程不能运行，CPU 空闲时也不能运行。</p>
<p>三种状态会涉及四种状态间的切换，在操作系统发现进程不能继续执行时会发生<code>状态1</code>的轮转，在某些系统中进程执行系统调用，例如 <code>pause</code>，来获取一个阻塞的状态。在其他系统中包括 UNIX，当进程从管道或特殊文件（例如终端）中读取没有可用的输入时，该进程会被自动终止。</p>
<p>转换 2 和转换 3 都是由进程调度程序（操作系统的一部分）引起的，进程本身不知道调度程序的存在。转换 2 的出现说明进程调度器认定当前进程已经运行了足够长的时间，是时候让其他进程运行 CPU 时间片了。当所有其他进程都运行过后，这时候该是让第一个进程重新获得 CPU 时间片的时候了，就会发生转换 3。</p>
<p>当进程等待的一个外部事件发生时（如从外部输入一些数据后），则发生转换 4。如果此时没有其他进程在运行，则立刻触发转换 3，该进程便开始运行，否则该进程会处于就绪阶段，等待 CPU 空闲后再轮到它运行。</p>
<p>从上面的观点引入了下面的模型</p>
<p><img src="https://pic.downk.cc/item/5f07e1cd14195aa594bda988.png"></p>
<p><strong>操作系统最底层的就是调度程序</strong>，在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上，调度程序只是一段非常小的程序。</p>
<h2 id="进程的实现"><a href="#进程的实现" class="headerlink" title="进程的实现"></a>进程的实现</h2><p>操作系统为了执行进程间的切换，会维护着一张表格，这张表就是 <code>进程表(process table)</code>。每个进程占用一个进程表项。该表项包含了进程状态的重要信息，包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息，以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息，从而保证该进程随后能再次启动，就像从未被中断过一样。</p>
<p>下面展示了一个典型系统中的关键字段</p>
<p><img src="https://pic.downk.cc/item/5f07e20714195aa594bdb842.jpg"></p>
<p>第一列内容与<code>进程管理</code>有关，第二列内容与 <code>存储管理</code>有关，第三列内容与<code>文件管理</code>有关。</p>
<p>存储管理的 text segment 、 data segment、stack segment 更多了解见下面这篇文章</p>
<p><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s?__biz=MzU2NDg0OTgyMA==&mid=2247484788&idx=1&sn=8a17224cabe09d3bd564dfdf22e2ff5d&chksm=fc45f887cb3271914f0e688a3cce4d7e3ce9077cdde199648e72aa92ad08fba2047b4483b7e8&token=504034995&lang=zh_CN&scene=21#wechat_redirect">程序员需要了解的硬核知识之汇编语言(全)</a></p>
<p>现在我们应该对进程表有个大致的了解了，就可以在对单个 CPU 上如何运行多个顺序进程的错觉做更多的解释。与每一 I&#x2F;O 类相关联的是一个称作 <code>中断向量(interrupt vector)</code> 的位置（靠近内存底部的固定区域）。它包含中断服务程序的入口地址。假设当一个磁盘中断发生时，用户进程 3 正在运行，则中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器压入堆栈，计算机随即跳转到中断向量所指示的地址。这就是硬件所做的事情。然后软件就随即接管一切剩余的工作。</p>
<p>当中断结束后，操作系统会调用一个 C 程序来处理中断剩下的工作。在完成剩下的工作后，会使某些进程就绪，接着调用调度程序，决定随后运行哪个进程。然后将控制权转移给一段汇编语言代码，为当前的进程装入寄存器值以及内存映射并启动该进程运行，下面显示了中断处理和调度的过程。</p>
<ol>
<li>硬件压入堆栈程序计数器等</li>
<li>硬件从中断向量装入新的程序计数器</li>
<li>汇编语言过程保存寄存器的值</li>
<li>汇编语言过程设置新的堆栈</li>
<li>C 中断服务器运行（典型的读和缓存写入）</li>
<li>调度器决定下面哪个程序先运行</li>
<li>C 过程返回至汇编代码</li>
<li>汇编语言过程开始运行新的当前进程</li>
</ol>
<p>一个进程在执行过程中可能被中断数千次，但关键每次中断后，被中断的进程都返回到与中断发生前完全相同的状态。</p>
<h1 id="线程"><a href="#线程" class="headerlink" title="线程"></a>线程</h1><p>在传统的操作系统中，每个进程都有一个地址空间和一个控制线程。事实上，这是大部分进程的定义。不过，在许多情况下，经常存在同一地址空间中运行多个控制线程的情形，这些线程就像是分离的进程。下面我们就着重探讨一下什么是线程</p>
<h2 id="线程的使用"><a href="#线程的使用" class="headerlink" title="线程的使用"></a>线程的使用</h2><p>或许这个疑问也是你的疑问，为什么要在进程的基础上再创建一个线程的概念，准确的说，这其实是进程模型和线程模型的讨论，回答这个问题，可能需要分三步来回答</p>
<ul>
<li>多线程之间会共享同一块地址空间和所有可用数据的能力，这是进程所不具备的</li>
<li>线程要比进程<code>更轻量级</code>，由于线程更轻，所以它比进程更容易创建，也更容易撤销。在许多系统中，创建一个线程要比创建一个进程快 10 - 100 倍。</li>
<li>第三个原因可能是性能方面的探讨，如果多个线程都是 CPU 密集型的，那么并不能获得性能上的增强，但是如果存在着大量的计算和大量的 I&#x2F;O 处理，拥有多个线程能在这些活动中彼此重叠进行，从而会加快应用程序的执行速度</li>
</ul>
<h2 id="多线程解决方案"><a href="#多线程解决方案" class="headerlink" title="多线程解决方案"></a>多线程解决方案</h2><p>现在考虑一个线程使用的例子：一个万维网服务器，对页面的请求发送给服务器，而所请求的页面发送回客户端。在多数 web 站点上，某些页面较其他页面相比有更多的访问。例如，索尼的主页比任何一个照相机详情介绍页面具有更多的访问，Web 服务器可以把获得大量访问的页面集合保存在内存中，避免到磁盘去调入这些页面，从而改善性能。这种页面的集合称为 <code>高速缓存(cache)</code>，高速缓存也应用在许多场合中，比如说 CPU 缓存。</p>
<p>上面是一个 web 服务器的组织方式，一个叫做 <code>调度线程(dispatcher thread)</code> 的线程从网络中读入工作请求，在调度线程检查完请求后，它会选择一个空闲的（阻塞的）工作线程来处理请求，通常是将消息的指针写入到每个线程关联的特殊字中。然后调度线程会唤醒正在睡眠中的工作线程，把工作线程的状态从阻塞态变为就绪态。</p>
<p>当工作线程启动后，它会检查请求是否在 web 页面的高速缓存中存在，这个高速缓存是所有线程都可以访问的。如果高速缓存不存在这个 web 页面的话，它会调用一个 <code>read</code> 操作从磁盘中获取页面并且阻塞线程直到磁盘操作完成。当线程阻塞在硬盘操作的期间，为了完成更多的工作，调度线程可能挑选另一个线程运行，也可能把另一个当前就绪的工作线程投入运行。</p>
<p>这种模型允许将服务器编写为顺序线程的集合，在分派线程的程序中包含一个死循环，该循环用来获得工作请求并且把请求派给工作线程。每个工作线程的代码包含一个从调度线程接收的请求，并且检查 web 高速缓存中是否存在所需页面，如果有，直接把该页面返回给客户，接着工作线程阻塞，等待一个新请求的到达。如果没有，工作线程就从磁盘调入该页面，将该页面返回给客户机，然后工作线程阻塞，等待一个新请求。</p>
<p>下面是调度线程和工作线程的代码，这里假设 TRUE 为常数 1 ，buf 和 page 分别是保存工作请求和 Web 页面的相应结构。</p>
<p><strong>调度线程的大致逻辑</strong></p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span>(TRUE)&#123;</span><br><span class="line"> get_next_request(&amp;buf);</span><br><span class="line"> handoff_work(&amp;buf);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>工作线程的大致逻辑</strong></p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span>(TRUE)&#123;</span><br><span class="line">  wait_for_work(&amp;buf);</span><br><span class="line">  look_for_page_in_cache(&amp;buf,&amp;page);</span><br><span class="line">  <span class="keyword">if</span>(page_not_in_cache(&amp;page))&#123;</span><br><span class="line">    read_page_from_disk(&amp;buf,&amp;page);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> _page(&amp;page);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="单线程解决方案"><a href="#单线程解决方案" class="headerlink" title="单线程解决方案"></a>单线程解决方案</h2><p>现在考虑没有多线程的情况下，如何编写 Web 服务器。我们很容易的就想象为单个线程了，Web 服务器的主循环获取请求并检查请求，并争取在下一个请求之前完成工作。在等待磁盘操作时，服务器空转，并且不处理任何到来的其他请求。结果会导致每秒中只有很少的请求被处理，所以这个例子能够说明多线程提高了程序的并行性并提高了程序的性能。</p>
<h2 id="状态机解决方案"><a href="#状态机解决方案" class="headerlink" title="状态机解决方案"></a>状态机解决方案</h2><p>到现在为止，我们已经有了两种解决方案，单线程解决方案和多线程解决方案，其实还有一种解决方案就是 <code>状态机解决方案</code>，它的流程如下</p>
<p>如果目前只有一个非阻塞版本的 read 系统调用可以使用，那么当请求到达服务器时，这个唯一的 read 调用的线程会进行检查，如果能够从高速缓存中得到响应，那么直接返回，如果不能，则启动一个非阻塞的磁盘操作</p>
<p>服务器在表中记录当前请求的状态，然后进入并获取下一个事件，紧接着下一个事件可能就是一个新工作的请求或是磁盘对先前操作的回答。如果是新工作的请求，那么就开始处理请求。如果是磁盘的响应，就从表中取出对应的状态信息进行处理。对于非阻塞式磁盘 I&#x2F;O 而言，这种响应一般都是信号中断响应。</p>
<p>每次服务器从某个请求工作的状态切换到另一个状态时，都必须显示的保存或者重新装入相应的计算状态。这里，每个计算都有一个被保存的状态，存在一个会发生且使得相关状态发生改变的事件集合，我们把这类设计称为<code>有限状态机(finite-state machine)</code>，有限状态机被广泛的应用在计算机科学中。</p>
<p>这三种解决方案各有各的特性，多线程使得顺序进程的思想得以保留下来，并且实现了并行性，但是顺序进程会阻塞系统调用；单线程服务器保留了阻塞系统的简易性，但是却放弃了性能。有限状态机的处理方法运用了非阻塞调用和中断，通过并行实现了高性能，但是给编程增加了困难。</p>
<h2 id="经典的线程模型"><a href="#经典的线程模型" class="headerlink" title="经典的线程模型"></a>经典的线程模型</h2><p>理解进程的另一个角度是，用某种方法把相关的资源集中在一起。进程有存放程序正文和数据以及其他资源的地址空间。这些资源包括打开的文件、子进程、即将发生的定时器、信号处理程序、账号信息等。把这些信息放在进程中会比较容易管理。</p>
<p>另一个概念是，进程中拥有一个执行的线程，通常简写为 <code>线程(thread)</code>。线程会有程序计数器，用来记录接着要执行哪一条指令；线程还拥有寄存器，用来保存线程当前正在使用的变量；线程还会有堆栈，用来记录程序的执行路径。尽管线程必须在某个进程中执行，但是进程和线程完完全全是两个不同的概念，并且他们可以分开处理。进程用于把资源集中在一起，而线程则是 CPU 上调度执行的实体。</p>
<p>线程给进程模型增加了一项内容，即在同一个进程中，允许彼此之间有较大的独立性且互不干扰。<strong>在一个进程中并行运行多个线程类似于在一台计算机上运行多个进程。在多个线程中，各个线程共享同一地址空间和其他资源。在多个进程中，进程共享物理内存、磁盘、打印机和其他资源。</strong>因为线程会包含有一些进程的属性，所以线程被称为<code>轻量的进程(lightweight processes)</code>。<code>多线程(multithreading)</code>一词还用于描述在同一进程中多个线程的情况。</p>
<p>下图我们可以看到三个传统的进程，每个进程有自己的地址空间和单个控制线程。每个线程都在不同的地址空间中运行</p>
<p><img src="https://pic.downk.cc/item/5f081aa514195aa594cd5889.png"></p>
<p>下图中，我们可以看到有一个进程三个线程的情况。每个线程都在相同的地址空间中运行。</p>
<p><img src="https://pic.downk.cc/item/5f081ac714195aa594cd6492.png"></p>
<p>线程不像是进程那样具备较强的独立性。同一个进程中的所有线程都会有完全一样的地址空间，这意味着它们也共享同样的全局变量。由于每个线程都可以访问进程地址空间内每个内存地址，<strong>因此一个线程可以读取、写入甚至擦除另一个线程的堆栈</strong>。线程之间除了共享同一内存空间外，还具有如下不同的内容</p>
<p><img src="https://pic.downk.cc/item/5f081aeb14195aa594cd7435.png"></p>
<p>和进程一样，线程可以处于下面这几种状态：<strong>运行中、阻塞、就绪和终止（进程图中没有画）</strong>。正在运行的线程拥有 CPU 时间片并且状态是运行中。一个被阻塞的线程会等待某个释放它的事件。例如，当一个线程执行从键盘读入数据的系统调用时，该线程就被阻塞直到有输入为止。线程通常会被阻塞，直到它等待某个外部事件的发生或者有其他线程来释放它。<strong>线程之间的状态转换和进程之间的状态转换是一样的</strong>。</p>
<p>每个线程都会有自己的堆栈，如下图所示</p>
<p><img src="https://pic.downk.cc/item/5f081b4914195aa594cd8f87.jpg"></p>
<h2 id="线程系统调用"><a href="#线程系统调用" class="headerlink" title="线程系统调用"></a>线程系统调用</h2><p>进程通常会从当前的某个单线程开始，然后这个线程通过调用一个库函数（比如 <code>thread_create</code>）创建新的线程。线程创建的函数会要求指定新创建线程的名称。创建的线程通常都返回一个线程标识符，该标识符就是新线程的名字。</p>
<p>当一个线程完成工作后，可以通过调用一个函数（比如 <code>thread_exit</code>）来退出。紧接着线程消失，状态变为终止，不能再进行调度。在某些线程的运行过程中，可以通过调用函数例如 <code>thread_join</code> ，表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种情况下，线程的创建和终止非常类似于进程的创建和终止。</p>
<p>另一个常见的线程是调用 <code>thread_yield</code>，它允许线程自动放弃 CPU 从而让另一个线程运行。这样一个调用还是很重要的，因为不同于进程，线程是无法利用时钟中断强制让线程让出 CPU 的。</p>
<p>为了使编写可移植线程程序成为可能，IEEE 在 IEEE 标准 1003.1c 中定义了线程标准。线程包被定义为 <code>Pthreads</code>。大部分的 UNIX 系统支持它。这个标准定义了 60 多种功能调用，一一列举不太现实，下面为你列举了一些常用的系统调用。</p>
<blockquote>
<p><strong>POSIX线程</strong>（通常称为<strong>pthreads</strong>）是一种独立于语言而存在的执行模型，以及并行执行模型。它允许程序控制时间上重叠的多个不同的工作流程。每个工作流程都称为一个线程，可以通过调用POSIX Threads API来实现对这些流程的创建和控制。可以把它理解为线程的标准。</p>
<p>POSIX Threads 的实现在许多类似且符合POSIX的操作系统上可用，例如 <strong>FreeBSD、NetBSD、OpenBSD、Linux、macOS、Android、Solaris</strong>，它在现有 Windows API 之上实现了<strong>pthread</strong>。</p>
<p>IEEE 是世界上最大的技术专业组织，致力于为人类的利益而发展技术。</p>
</blockquote>
<table>
<thead>
<tr>
<th align="left">线程调用</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">pthread_create</td>
<td align="left">创建一个新线程</td>
</tr>
<tr>
<td align="left">pthread_exit</td>
<td align="left">结束调用的线程</td>
</tr>
<tr>
<td align="left">pthread_join</td>
<td align="left">等待一个特定的线程退出</td>
</tr>
<tr>
<td align="left">pthread_yield</td>
<td align="left">释放 CPU 来运行另外一个线程</td>
</tr>
<tr>
<td align="left">pthread_attr_init</td>
<td align="left">创建并初始化一个线程的属性结构</td>
</tr>
<tr>
<td align="left">pthread_attr_destory</td>
<td align="left">删除一个线程的属性结构</td>
</tr>
</tbody></table>
<p>所有的 Pthreads 都有特定的属性，每一个都含有标识符、一组寄存器（包括程序计数器）和一组存储在结构中的属性。这个属性包括堆栈大小、调度参数以及其他线程需要的项目。</p>
<p>新的线程会通过 <code>pthread_create</code> 创建，新创建的线程的标识符会作为函数值返回。这个调用非常像是 UNIX 中的 <code>fork</code> 系统调用（除了参数之外），其中线程标识符起着 <code>PID</code> 的作用，这么做的目的是为了和其他线程进行区分。</p>
<p>当线程完成指派给他的工作后，会通过 <code>pthread_exit</code> 来终止。这个调用会停止线程并释放堆栈。</p>
<p>一般一个线程在继续运行前需要等待另一个线程完成它的工作并退出。可以通过 <code>pthread_join</code> 线程调用来等待别的特定线程的终止。而要等待线程的线程标识符作为一个参数给出。</p>
<p>有时会出现这种情况：一个线程逻辑上没有阻塞，但感觉上它已经运行了足够长的时间并且希望给另外一个线程机会去运行。这时候可以通过 <code>pthread_yield</code> 来完成。</p>
<p>下面两个线程调用是处理属性的。<code>pthread_attr_init</code> 建立关联一个线程的属性结构并初始化成默认值，这些值（例如优先级）可以通过修改属性结构的值来改变。</p>
<p>最后，<code>pthread_attr_destroy</code> 删除一个线程的结构，释放它占用的内存。它不会影响调用它的线程，这些线程会一直存在。</p>
<p>为了更好的理解 pthread 是如何工作的，考虑下面这个例子</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;pthread.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> NUMBER_OF_THREADS 10</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> *<span class="title function_">print_hello_world</span><span class="params">(vvoid *tid)</span>&#123;</span><br><span class="line">  <span class="comment">/* 输出线程的标识符，然后退出 */</span></span><br><span class="line">  <span class="built_in">printf</span>(<span class="string">&quot;Hello World. Greetings from thread %d\n&quot;</span>,tid);</span><br><span class="line">  pthread_exit(<span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc,<span class="type">char</span> *argv[])</span>&#123;</span><br><span class="line">  <span class="comment">/* 主程序创建 10 个线程，然后退出 */</span></span><br><span class="line">  <span class="type">pthread_t</span> threads[NUMBER_OF_THREADS];</span><br><span class="line">  <span class="type">int</span> status,i;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>;i &lt; NUMBER_OF_THREADS;i++)&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;Main here. Creating thread %d\n&quot;</span>,i);</span><br><span class="line">    status = pthread_create(&amp;threads[i], <span class="literal">NULL</span>, print_hello_world, (<span class="type">void</span> *)i);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(status != <span class="number">0</span>)&#123;</span><br><span class="line">      <span class="built_in">printf</span>(<span class="string">&quot;Oops. pthread_create returned error code %d\n&quot;</span>,status);</span><br><span class="line">      <span class="built_in">exit</span>(<span class="number">-1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">exit</span>(<span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>主线程在宣布它的职责之后，循环 <code>NUMBER_OF_THREADS</code> 次，每次创建一个新的线程。如果线程创建失败，会打印出一条信息后退出。在创建完成所有的工作后，主程序退出。</p>
<h2 id="线程实现"><a href="#线程实现" class="headerlink" title="线程实现"></a>线程实现</h2><p>主要有三种实现方式</p>
<ul>
<li>在用户空间中实现线程；</li>
<li>在内核空间中实现线程；</li>
<li>在用户和内核空间中混合实现线程。</li>
</ul>
<p>下面我们分开讨论一下</p>
<h3 id="在用户空间中实现线程"><a href="#在用户空间中实现线程" class="headerlink" title="在用户空间中实现线程"></a>在用户空间中实现线程</h3><p>第一种方法是把整个线程包放在用户空间中，内核对线程一无所知，它不知道线程的存在。所有的这类实现都有同样的通用结构</p>
<p><img src="https://pic.downk.cc/item/5f08209814195aa594cf27e1.png"></p>
<p>线程在运行时系统之上运行，运行时系统是管理线程过程的集合，包括前面提到的四个过程：pthread_create, pthread_exit, pthread_join 和 pthread_yield。</p>
<blockquote>
<p><code>运行时系统(Runtime System)</code> 也叫做运行时环境，该运行时系统提供了程序在其中运行的环境。此环境可能会解决许多问题，包括应用程序内存的布局，程序如何访问变量，在过程之间传递参数的机制，与操作系统的接口等等。编译器根据特定的运行时系统进行假设以生成正确的代码。通常，运行时系统将负责设置和管理堆栈，并且会包含诸如垃圾收集，线程或语言内置的其他动态的功能。</p>
</blockquote>
<p>在用户空间管理线程时，每个进程需要有其专用的<code>线程表(thread table)</code>，用来跟踪该进程中的线程。这些表和内核中的进程表类似，不过它仅仅记录各个线程的属性，如每个线程的程序计数器、堆栈指针、寄存器和状态。该线程表由运行时系统统一管理。当一个线程转换到就绪状态或阻塞状态时，在该线程表中存放重新启动该线程的所有信息，与内核在进程表中存放的信息完全一样。</p>
<h4 id="在用户空间实现线程的优势"><a href="#在用户空间实现线程的优势" class="headerlink" title="在用户空间实现线程的优势"></a>在用户空间实现线程的优势</h4><p>在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势：考虑如果在线程完成时或者是在调用 <code>pthread_yield</code> 时，必要时会进程线程切换，然后线程的信息会被保存在运行时环境所提供的线程表中，然后，线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是<code>本地过程</code>，<strong>所以启动他们比进行内核调用效率更高。因而不需要切换到内核，也就不需要上下文切换，也不需要对内存高速缓存进行刷新，因为线程调度非常便捷，因此效率比较高</strong>。</p>
<p>在用户空间实现线程还有一个优势就是<strong>它允许每个进程有自己定制的调度算法</strong>。例如在某些应用程序中，那些具有垃圾收集线程的应用程序（知道是谁了吧）就不用担心自己线程会不会在不合适的时候停止，这是一个优势。用户线程还具有较好的可扩展性，因为内核空间中的内核线程需要一些表空间和堆栈空间，如果内核线程数量比较大，容易造成问题。</p>
<h4 id="在用户空间实现线程的劣势"><a href="#在用户空间实现线程的劣势" class="headerlink" title="在用户空间实现线程的劣势"></a>在用户空间实现线程的劣势</h4><p>尽管在用户空间实现线程会具有一定的性能优势，但是劣势还是很明显的，你如何实现<code>阻塞系统调用</code>呢？假设在还没有任何键盘输入之前，一个线程读取键盘，让线程进行系统调用是不可能的，因为这会停止所有的线程。所以，<strong>使用线程的一个目标是能够让线程进行阻塞调用，并且要避免被阻塞的线程影响其他线程</strong>。</p>
<p>与阻塞调用类似的问题是<code>缺页中断</code>问题，实际上，计算机并不会把所有的程序都一次性的放入内存中，如果某个程序发生函数调用或者跳转指令到了一条不在内存的指令上，就会发生页面故障，而操作系统将到磁盘上取回这个丢失的指令，这就称为<code>缺页故障</code>。而在对所需的指令进行读入和执行时，相关的进程就会被阻塞。如果只有一个线程引起页面故障，内核由于甚至不知道有线程存在，通常会把整个进程阻塞直到磁盘 I&#x2F;O 完成为止，尽管其他的线程是可以运行的。</p>
<p>另外一个问题是，如果一个线程开始运行，该线程所在进程中的其他线程都不能运行，除非第一个线程自愿的放弃 CPU，在一个单进程内部，没有时钟中断，所以不可能使用轮转调度的方式调度线程。除非其他线程能够以自己的意愿进入运行时环境，否则调度程序没有可以调度线程的机会。</p>
<h3 id="在内核中实现线程"><a href="#在内核中实现线程" class="headerlink" title="在内核中实现线程"></a>在内核中实现线程</h3><p>现在我们考虑使用内核来实现线程的情况，此时不再需要运行时环境了。另外，每个进程中也没有线程表。相反，在内核中会有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时，它会进行一个系统调用，这个系统调用通过对线程表的更新来完成线程创建或销毁工作。</p>
<p><img src="https://pic.downk.cc/item/5f08285314195aa594d17ebc.png"></p>
<p>内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同，但是位置却被放在了内核中而不是用户空间中。另外，内核还维护了一张进程表用来跟踪系统状态。</p>
<p>所有能够阻塞的调用都会通过系统调用的方式来实现，当一个线程阻塞时，内核可以进行选择，是运行在同一个进程中的另一个线程（如果有就绪线程的话）还是运行一个另一个进程中的线程。但是在用户实现中，运行时系统始终运行自己的线程，直到内核剥夺它的 CPU 时间片（或者没有可运行的线程存在了）为止。</p>
<p>由于在内核中创建或者销毁线程的开销比较大，所以某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时，就把它标志为不可运行的状态，但是其内部结构没有受到影响。稍后，在必须创建一个新线程时，就会重新启用旧线程，把它标志为可用状态。</p>
<p>如果某个进程中的线程造成缺页故障后，内核很容易的就能检查出来是否有其他可运行的线程，如果有的话，在等待所需要的页面从磁盘读入时，就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大，所以如果线程的操作（创建、终止）比较多，就会带来很大的开销。</p>
<h3 id="混合实现"><a href="#混合实现" class="headerlink" title="混合实现"></a>混合实现</h3><p>结合用户空间和内核空间的优点，设计人员采用了一种<code>内核级线程</code>的方式，然后将用户级线程与某些或者全部内核线程多路复用起来</p>
<p><img src="https://pic.downk.cc/item/5f082e8114195aa594d405c2.png"></p>
<p>在这种模型中，编程人员可以自由控制用户线程和内核线程的数量，具有很大的灵活度。采用这种方法，内核只识别内核级线程，并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。</p>
<h1 id="进程间通信"><a href="#进程间通信" class="headerlink" title="进程间通信"></a>进程间通信</h1><p>进程是需要频繁的和其他进程进行交流的。例如，在一个 shell 管道中，第一个进程的输出必须传递给第二个进程，这样沿着管道进行下去。因此，进程之间如果需要通信的话，必须要使用一种良好的数据结构以至于不能被中断。下面我们会一起讨论有关 <code>进程间通信(Inter Process Communication, IPC)</code> 的问题。</p>
<p>关于进程间的通信，这里有三个问题</p>
<ul>
<li>上面提到了第一个问题，那就是一个进程如何传递消息给其他进程。</li>
<li>第二个问题是如何确保两个或多个线程之间不会相互干扰。例如，两个航空公司都试图为不同的顾客抢购飞机上的最后一个座位。</li>
<li>第三个问题是数据的先后顺序的问题，如果进程 A 产生数据并且进程 B 打印数据。则进程 B 打印数据之前需要先等 A 产生数据后才能够进行打印。</li>
</ul>
<p>需要注意的是，这三个问题中的后面两个问题同样也适用于线程</p>
<p>第一个问题在线程间比较好解决，因为它们共享一个地址空间，它们具有相同的运行时环境，可以想象你在用高级语言编写多线程代码的过程中，线程通信问题是不是比较容易解决？</p>
<p>另外两个问题也同样适用于线程，同样的问题可用同样的方法来解决。我们后面会慢慢讨论这三个问题，你现在脑子中大致有个印象即可。</p>
<h2 id="竞态条件"><a href="#竞态条件" class="headerlink" title="竞态条件"></a>竞态条件</h2><p>在一些操作系统中，协作的进程可能共享一些彼此都能读写的公共资源。公共资源可能在内存中也可能在一个共享文件。为了讲清楚进程间是如何通信的，这里我们举一个例子：一个后台打印程序。当一个进程需要打印某个文件时，它会将文件名放在一个特殊的<code>后台目录(spooler directory)</code>中。另一个进程 <code>打印后台进程(printer daemon)</code> 会定期的检查是否需要文件被打印，如果有的话，就打印并将该文件名从目录下删除。</p>
<p>假设我们的后台目录有非常多的 <code>槽位(slot)</code>，编号依次为 0，1，2，…，每个槽位存放一个文件名。同时假设有两个共享变量：<code>out</code>，指向下一个需要打印的文件；<code>in</code>，指向目录中下个空闲的槽位。可以把这两个文件保存在一个所有进程都能访问的文件中，该文件的长度为两个字。在某一时刻，0 至 3 号槽位空，4 号至 6 号槽位被占用。在同一时刻，进程 A 和 进程 B 都决定将一个文件排队打印，情况如下</p>
<p><img src="https://pic.downk.cc/item/5f08340614195aa594d5980f.png"></p>
<p>进程 A 读到 in 的值为 7，将 7 存在一个局部变量 <code>next_free_slot</code> 中。此时发生一次时钟中断，CPU 认为进程 A 已经运行了足够长的时间，决定切换到进程 B 。进程 B 也读取 in 的值，发现是 7，然后进程 B 将 7 写入到自己的局部变量 <code>next_free_slot</code> 中，在这一时刻两个进程都认为下一个可用槽位是 7 。</p>
<p>进程 B 现在继续运行，它会将打印文件名写入到 slot 7 中，然后把 in 的指针更改为 8 ，然后进程 B 离开去做其他的事情</p>
<p>现在进程 A 开始恢复运行，由于进程 A 通过检查 <code>next_free_slot</code>也发现 slot 7 的槽位是空的，于是将打印文件名存入 slot 7 中，然后把 in 的值更新为 8 ，由于 slot 7 这个槽位中已经有进程 B 写入的值，所以进程 A 的打印文件名会把进程 B 的文件覆盖，由于打印机内部是无法发现是哪个进程更新的，它的功能比较局限，所以这时候进程 B 永远无法打印输出，类似这种情况，**即两个或多个线程同时对一共享数据进行修改，从而影响程序运行的正确性时，这种就被称为竞态条件(race condition)**。调试竞态条件是一种非常困难的工作，因为绝大多数情况下程序运行良好，但在极少数的情况下会发生一些无法解释的奇怪现象。</p>
<h2 id="临界区"><a href="#临界区" class="headerlink" title="临界区"></a>临界区</h2><p>[toc]</p>
<blockquote>
<p><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s?__biz=MzU2NDg0OTgyMA==&mid=2247485619&idx=1&sn=819fffc4380b4e976f541def5ed805f3&chksm=fc45f540cb327c560e4eb5747183faec42fcc77c6061effaf36e28faef689f920a54d5a78eeb&token=1981249032&lang=zh_CN#rd">原文章地址</a></p>
</blockquote>

        </div>

        
            <section class="post-copyright">
                
                    <p class="copyright-item">
                        <span>Author:</span>
                        <span>Ian Tang</span>
                    </p>
                
                
                    <p class="copyright-item">
                        <span>Permalink:</span>
                        <span><a href="https://tkhfree.github.io/2022/11/27/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F-2022-11-27/">https://tkhfree.github.io/2022/11/27/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F-2022-11-27/</a></span>
                    </p>
                
                
                    <p class="copyright-item">
                        <span>License:</span>
                        <span>Copyright (c) 2019 <a target="_blank" rel="noopener" href="http://creativecommons.org/licenses/by-nc/4.0/">CC-BY-NC-4.0</a> LICENSE</span>
                    </p>
                
                
                     <p class="copyright-item">
                         <span>Slogan:</span>
                         <span>Do you believe in <strong>DESTINY</strong>?</span>
                     </p>
                

            </section>
        
        <section class="post-tags">
            <div>
                <span>Tag(s):</span>
                <span class="tag">
                    
                    
                        <a href="/tags/c-c/"># c/c++</a>
                    
                        <a href="/tags/system/"># system</a>
                    
                        
                </span>
            </div>
            <div>
                <a href="javascript:window.history.back();">back</a>
                <span>· </span>
                <a href="/">home</a>
            </div>
        </section>
        <section class="post-nav">
            
                <a class="prev" rel="prev" href="/2022/11/27/Nginx%E5%AE%89%E8%A3%85-2022-11-27/">Nginx安装</a>
            
            
            <a class="next" rel="next" href="/2022/11/27/Neutron%E5%AD%A6%E4%B9%A0-2022-11-27/">Neutron学习</a>
            
        </section>


    </article>
</div>

            </div>
            <footer id="footer" class="footer">
    <div class="copyright">
        <span>© Ian Tang | Powered by <a href="https://hexo.io" target="_blank">Hexo</a> & <a href="https://github.com/Siricee/hexo-theme-Chic" target="_blank">Chic</a></span>
    </div>
</footer>

    </div>
</body>

</html>